README.md

# ExUnion
[![CI](https://github.com/sascha-wolf/ex_union/workflows/CI/badge.svg)](https://github.com/sascha-wolf/ex_union/actions?query=workflow%3ACI+branch%3Amain)
[![Coverage Status](https://coveralls.io/repos/github/sascha-wolf/ex_union/badge.svg?branch=main)](https://coveralls.io/github/sascha-wolf/ex_union?branch=main)
[![Hexdocs.pm](https://img.shields.io/badge/hexdocs-online-blue)](https://hexdocs.pm/ex_union)
[![Hex.pm](https://img.shields.io/hexpm/v/ex_union.svg)](https://hex.pm/packages/ex_union)
[![Hex.pm Downloads](https://img.shields.io/hexpm/dt/ex_union)](https://hex.pm/packages/ex_union)

Tagged Unions for Elixir.
Just that.

## Overview

- [Overview](#overview)
- [Installation](#installation)
- [Motivation](#motivation)
- [Usage](#usage)
  - [Example: Multiple Fields](#example-multiple-fields)
  - [Example: Adding Type Specifications](#example-adding-type-specifications)
  - [Example: Adding Recursive Type Specifications](#example-adding-recursive-type-specifications)
  - [Example: If you'd write all this by hand](#example-if-youd-write-all-this-by-hand)
- [Comparison](#comparison)
  - [`Algae`](#algae)
  - [`Ok` or `Wormhole`](#ok-or-wormhole)
- [Roadmap](#roadmap)
- [Contributing](#contributing)

## Installation

Add [`ex_union`][hex] to your list of dependencies in `mix.exs`:

```elixir
def deps do
  [
    {:ex_union, "~> 0.1.0"}
  ]
end
```

Differences between the versions are explained in [the Changelog](./CHANGELOG.md).

Documentation gets generated with [ExDoc](https://github.com/elixir-lang/ex_doc) and can be viewed at [HexDocs][hexdocs].

## Motivation

`ExUnion` is meant to be a lightweight, elixir-y implementation of [tagged unions](https://en.wikipedia.org/wiki/Tagged_union) (also called variant, discriminated union, sum type, etc.).

While conventionally Elixir tends to promote using tuples to model tagged unions - the `{:ok, ...} | {:error, ...}` pattern being a good example of that - this approach arguably lacks expressiveness, especially when modeling non-trivial unions.
An alternative is to employ structs to model the individual cases of a tagged union, which works nicely but has the disadvantage of requiring significant boilerplate code.

`ExUnion` attempts to bridge this gap by generating the necessary boilerplate (and a bit more) through a concise albeit opinionated DSL.

## Usage

To get an idea on how you can use `ExUnion` let's look at an example:

```elixir
defmodule Maybe do
  import ExUnion

  defunion some(value) | none
end
```

The `defunion` macro takes a type-spec similar definition and generates a bunch of code from it.
Let's see how we can use `Maybe` now, shall we?

```elixir
iex> Maybe.none()
%Maybe.None{}

iex> Maybe.some("string!")
%Maybe.Some{value: "string!"}

# Requiring is necessary since `is_maybe` is a guard (defguard)
iex> require Maybe
iex> Maybe.is_maybe("What's the meaning of life, the universe, and everything?")
false
iex> Maybe.is_maybe(42)
false
iex> Maybe.is_maybe(Maybe.none())
true
```

As you can see `ExUnion` generates a number of things from the definition:

- a struct for each case of the union (including type specs)
- a shortcut function for each case to create said struct (including `@spec`s)
- a shortcut type spec for each case and the general union (`t`, `union`, `union_<case>`)
- a guard that returns `true` if the given value is part of the union

Check out the additional examples below to get a better impression of what `ExUnion` offers.

### Example: Multiple Fields

```elixir
defmodule Shape do
  import ExUnion

  defunion circle(radius)
           | square(side)
           | rectangle(width, height)
end

iex> Shape.circle(3)
%Shape.Circle{radius: 3}

iex> Shape.square(side: 4)
%Shape.Square{side: 4}

iex> Shape.rectangle(4, 2)
%Shape.Rectangle{width: 4, height: 2}

iex> Shape.rectangle(height: 2, width: 4)
%Shape.Rectangle{width: 4, height: 2}
```

### Example: Adding Type Specifications

```elixir
defmodule Color do
  import ExUnion

  defunion hex(string :: String.t)
           | rgb(red :: 0..255, green :: 0..255, blue :: 0..255)
           | rgba(red :: 0..255, green :: 0..255, blue :: 0..255, alpha :: float)
           | hsl(hue :: 0..360, saturation :: float, lightness :: float)
           | hsla(hue :: 0..360, saturation :: float, lightness :: float, alpha :: float)
end
```

### Example: Adding Recursive Type Specifications

```elixir
defmodule IntegerTree do
  import ExUnion

  # You can also use `t` instead of `union` if you prefer
  defunion leaf | node(integer :: integer, left :: union, right :: union)
end
```

If necessary you can ever refer to individual cases of the union.
Let's revisit the `Color` example for above and how we can use recursive types to reuse the `rbg` and `hsl` definitions:

```elixir
defmodule Color do
  import ExUnion

  defunion hex(string :: String.t)
           | rgb(red :: 0..255, green :: 0..255, blue :: 0..255)
           | rgba(rgb :: union_rgb, alpha :: float)
           | hsl(hue :: 0..360, saturation :: float, lightness :: float)
           | hsla(hsl :: union_hsl, alpha :: float)
end
```

### Example: If you'd write all this by hand

To give you more of an idea on the kind of code `ExUnion` generates for you, let's look at what you'd have to write out to get something equivalent.
For this we'll use the `Maybe` example from earlier again.

```elixir
defmodule Maybe do
  @type t :: union
  @type union :: Maybe.None.t() | Maybe.Some.t()
  @type union_some :: Maybe.Some.t()
  @type union_none :: Maybe.None.t()

  defmodule Some do
    @type t :: %__MODULE__{value: any}
    defstruct [:value]

    @spec new(fields :: %{value: any}) :: t
    def new(fields) when is_map(fields) and :erlang.is_map_key(:value, fields) do
      struct!(__MODULE__, fields)
    end

    @spec new(fields :: [value: any]) :: t
    def new([{field, _} | _] = fields) when field in [:value] do
      struct!(__MODULE__, fields)
    end

    @spec new(value :: any) :: t
    def new(value) do
      %__MODULE__{value: value}
    end
  end

  defmodule None do
    @type t :: %__MODULE__{}
    defstruct []

    @spec new() :: t
    def new() do
      %__MODULE__{}
    end
  end

  defdelegate some(value), to: Maybe.Some, as: :new
  defdelegate none(), to: Maybe.None, as: :new

  defguard is_maybe(value)
           when is_map(value) and :erlang.is_map_key(:__struct__, value) and
                  :erlang.map_get(:__struct__, value) in [Some, None]
end
```

Out of a single line of `defunion some(value) | none` `ExUnion` generated over 30 lines of code.
And while the specifics of the generated code are opinionated in places, they do have a lot lower information density than the `defunion` line.

## Comparison

`ExUnion` can be compared to a number of other libraries.

### [`Algae`][elixir:algae]

[`Algae`][elixir:algae] offers for "algebraic data types for Elixir".

Some people might prefer that, and that's perfectly fine!
I think [`Algae`][elixir:algae] (and it's big brother [`Witchcraft`][elixir:witchcraft]) are amazing projects and should be used more - but I also think that they come with a lot of inborn complexity.

Not everybody is familiar with "algebraic data types" and arguably not everybody needs to be!
But on the other hand there's a lot of goodness in the tools they bring to the table.

[`Algea`][elixir:algae] also offers its own flavor of tagged unions (or rather sum types) but also with more than that.
`ExUnion` by design __only__ implements tagged unions and nothing more - as they are a tool most developers probably are familiar with - in an attempt to be as approachable and self-explanatory as possible.

At some point you and/or your team might decide to take the next step and use [`Algae`][elixir:algae] or even [`Witchcraft`][elixir:witchcraft] and `ExUnion` will be happy to have been part of your journey.
Or maybe `ExUnion` is all you need and that would be fine too.

### [`Ok`][elixir:ok] or [`Wormhole`][elixir:wormhole]

[`Ok`][elixir:ok] and [`Wormhole`][elixir:wormhole] both aim to provide additional tools to work with Elixir's most well-known tagged union: `{:ok, value}` and `{:error, reason}`.
But they do only that.

If you want more tools to deal with `{:ok, value}` and `{:error, reason}` tuples, then they are great libraries.
But if you want additional tools to model similar tagged unions, then these libraries don't help you.

`ExUnion` doesn't pretend to help you with `{:ok, value}` / `{:error, reason}`.
This isn't the motivation behind the project.
It does however give you more power to escape the limits of using tagged tuples to model unions.

## Roadmap

- [ ] Figure out a way to derive protocol implementations for union structs (e.g. for `Jason`)

## Contributing

Contributions are always welcome but please read [our contribution guidelines](./CONTRIBUTING.md) before doing so.

[elixir:algae]: https://github.com/witchcrafters/algae
[elixir:ok]: https://github.com/CrowdHailer/OK
[elixir:witchcraft]: https://github.com/witchcrafters/witchcraft
[elixir:wormhole]: https://github.com/renderedtext/wormhole
[hex]: https://hex.pm/packages/ex_union
[hexdocs]: https://hexdocs.pm/ex_union