ExUnion - Tagged Unions for Elixir. Just that

ExUnion is meant to be a lightweight, elixir-y implementation of tagged unions (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. Below the following links you can see an example for an idea on how you might use ExUnion.

defmodule Maybe do
  import ExUnion

  defunion some(value) | none
end

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

For more examples take a look at the project’s README.

15 Likes

Pretty nice, I do like it.

Is the following usage considered asserting on information that’s an implementation detail? Or this is exactly how your library should be used when it comes to pattern matching?

def do_stuff_with(%Maybe.None{}), do: :dont_do_anything
def do_stuff_with(%Maybe.Some{...}), do: :do_the_thing
2 Likes

That’s exactly how it’s supposed to be used. It’s also why the generated code contains type specs to more readily reveal the underlying structure.

Guess I could be more explicit in the docs about that.

1 Like

The docs are good, I am simply making sure because e.g. Ecto still has some structs whose fields are well-known but it’s still not recommended to rely on them always being there. Hence my question.

Thanks for clarifying. :+1:

1 Like

v0.1.1 - Resolve dialyzer warnings

Changed

  • #2: Resolve dialyzer warnings

Link to GitHub release

Thanks for the splendid library! Now the choice types can be expressed even shorter than in F# (comparing to the examples from Domain Modeling Made Functional book by Scott Wlaschin).

1 Like

v0.1.2 - Include .formatter.exs in the published package

Changed

  • Included .formatter.exs in the published package to enable import_deps: [:ex_union]

Link to GitHub release

This seems like a nice library.

One thing I’m wondering is how to do custom validations. For example in your color example:

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

Let’s say I wanted to validate that the hex value passed in has the correct format.

So is there a way to create a custom constructor for the Color.hex?

Not yet, it’s one of the things I’d like to explore, in addition to protocol derivation.

3 Likes