{"id":32166491,"url":"https://github.com/alexocode/ex_union","last_synced_at":"2026-02-20T00:31:52.449Z","repository":{"id":61560194,"uuid":"475978280","full_name":"alexocode/ex_union","owner":"alexocode","description":"Tagged unions for Elixir. Just that.","archived":false,"fork":false,"pushed_at":"2024-04-18T16:17:22.000Z","size":103,"stargazers_count":43,"open_issues_count":0,"forks_count":1,"subscribers_count":1,"default_branch":"main","last_synced_at":"2025-12-13T01:59:12.942Z","etag":null,"topics":["discriminated-unions","elixir","hex","sum-types","tagged-unions"],"latest_commit_sha":null,"homepage":"","language":"Elixir","has_issues":true,"has_wiki":null,"has_pages":null,"mirror_url":null,"source_name":null,"license":"mit","status":null,"scm":"git","pull_requests_enabled":true,"icon_url":"https://github.com/alexocode.png","metadata":{"files":{"readme":"README.md","changelog":"CHANGELOG.md","contributing":"CONTRIBUTING.md","funding":null,"license":"LICENSE.md","code_of_conduct":null,"threat_model":null,"audit":null,"citation":null,"codeowners":null,"security":null,"support":null,"governance":null,"roadmap":null,"authors":null,"dei":null}},"created_at":"2022-03-30T17:12:17.000Z","updated_at":"2025-06-19T20:50:29.000Z","dependencies_parsed_at":"2024-04-18T18:14:22.816Z","dependency_job_id":"f59b6714-73af-4818-995e-6f1662adb71b","html_url":"https://github.com/alexocode/ex_union","commit_stats":null,"previous_names":["alexocode/ex_union","sascha-wolf/ex_union"],"tags_count":3,"template":false,"template_full_name":null,"purl":"pkg:github/alexocode/ex_union","repository_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/alexocode%2Fex_union","tags_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/alexocode%2Fex_union/tags","releases_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/alexocode%2Fex_union/releases","manifests_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/alexocode%2Fex_union/manifests","owner_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/owners/alexocode","download_url":"https://codeload.github.com/alexocode/ex_union/tar.gz/refs/heads/main","sbom_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/alexocode%2Fex_union/sbom","scorecard":null,"host":{"name":"GitHub","url":"https://github.com","kind":"github","repositories_count":286080680,"owners_count":29637409,"icon_url":"https://github.com/github.png","version":null,"created_at":"2022-05-30T11:31:42.601Z","updated_at":"2026-02-19T22:32:43.237Z","status":"ssl_error","status_checked_at":"2026-02-19T22:32:38.330Z","response_time":117,"last_error":"SSL_connect returned=1 errno=0 peeraddr=140.82.121.6:443 state=error: unexpected eof while reading","robots_txt_status":"success","robots_txt_updated_at":"2025-07-24T06:49:26.215Z","robots_txt_url":"https://github.com/robots.txt","online":false,"can_crawl_api":true,"host_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub","repositories_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories","repository_names_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repository_names","owners_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/owners"}},"keywords":["discriminated-unions","elixir","hex","sum-types","tagged-unions"],"created_at":"2025-10-21T15:06:30.022Z","updated_at":"2026-02-20T00:31:52.437Z","avatar_url":"https://github.com/alexocode.png","language":"Elixir","funding_links":[],"categories":[],"sub_categories":[],"readme":"# ExUnion\n[![CI](https://github.com/alexocode/ex_union/workflows/CI/badge.svg)](https://github.com/alexocode/ex_union/actions?query=workflow%3ACI+branch%3Amain)\n[![Coverage Status](https://coveralls.io/repos/github/alexocode/ex_union/badge.svg?branch=main)](https://coveralls.io/github/alexocode/ex_union?branch=main)\n[![Hexdocs.pm](https://img.shields.io/badge/hexdocs-online-blue)](https://hexdocs.pm/ex_union)\n[![Hex.pm](https://img.shields.io/hexpm/v/ex_union.svg)](https://hex.pm/packages/ex_union)\n[![Hex.pm Downloads](https://img.shields.io/hexpm/dt/ex_union)](https://hex.pm/packages/ex_union)\n\nTagged Unions for Elixir.\nJust that.\n\n## Overview\n\n- [Overview](#overview)\n- [Installation](#installation)\n- [Motivation](#motivation)\n- [Usage](#usage)\n  - [Example: Multiple Fields](#example-multiple-fields)\n  - [Example: Adding Type Specifications](#example-adding-type-specifications)\n  - [Example: Adding Recursive Type Specifications](#example-adding-recursive-type-specifications)\n  - [Example: If you'd write all this by hand](#example-if-youd-write-all-this-by-hand)\n- [Comparison](#comparison)\n  - [`Algae`](#algae)\n  - [`Ok` or `Wormhole`](#ok-or-wormhole)\n- [Roadmap](#roadmap)\n- [Contributing](#contributing)\n\n## Installation\n\nAdd [`ex_union`][hex] to your list of dependencies in `mix.exs`:\n\n```elixir\ndef deps do\n  [\n    {:ex_union, \"~\u003e 0.1.0\"}\n  ]\nend\n```\n\nDifferences between the versions are explained in [the Changelog](./CHANGELOG.md).\n\nDocumentation gets generated with [ExDoc](https://github.com/elixir-lang/ex_doc) and can be viewed at [HexDocs][hexdocs].\n\n## Motivation\n\n`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.).\n\nWhile 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.\nAn 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.\n\n`ExUnion` attempts to bridge this gap by generating the necessary boilerplate (and a bit more) through a concise albeit opinionated DSL.\n\n## Usage\n\nTo get an idea on how you can use `ExUnion` let's look at an example:\n\n```elixir\ndefmodule Maybe do\n  import ExUnion\n\n  defunion some(value) | none\nend\n```\n\nThe `defunion` macro takes a type-spec similar definition and generates a bunch of code from it.\nLet's see how we can use `Maybe` now, shall we?\n\n```elixir\niex\u003e Maybe.none()\n%Maybe.None{}\n\niex\u003e Maybe.some(\"string!\")\n%Maybe.Some{value: \"string!\"}\n\n# Requiring is necessary since `is_maybe` is a guard (defguard)\niex\u003e require Maybe\niex\u003e Maybe.is_maybe(\"What's the meaning of life, the universe, and everything?\")\nfalse\niex\u003e Maybe.is_maybe(42)\nfalse\niex\u003e Maybe.is_maybe(Maybe.none())\ntrue\n```\n\nAs you can see `ExUnion` generates a number of things from the definition:\n\n- a struct for each case of the union (including type specs)\n- a shortcut function for each case to create said struct (including `@spec`s)\n- a shortcut type spec for each case and the general union (`t`, `union`, `union_\u003ccase\u003e`)\n- a guard that returns `true` if the given value is part of the union\n\nCheck out the additional examples below to get a better impression of what `ExUnion` offers.\n\n### Example: Multiple Fields\n\n```elixir\ndefmodule Shape do\n  import ExUnion\n\n  defunion circle(radius)\n           | square(side)\n           | rectangle(width, height)\nend\n\niex\u003e Shape.circle(3)\n%Shape.Circle{radius: 3}\n\niex\u003e Shape.square(side: 4)\n%Shape.Square{side: 4}\n\niex\u003e Shape.rectangle(4, 2)\n%Shape.Rectangle{width: 4, height: 2}\n\niex\u003e Shape.rectangle(height: 2, width: 4)\n%Shape.Rectangle{width: 4, height: 2}\n```\n\n### Example: Adding Type Specifications\n\n```elixir\ndefmodule Color do\n  import ExUnion\n\n  defunion hex(string :: String.t)\n           | rgb(red :: 0..255, green :: 0..255, blue :: 0..255)\n           | rgba(red :: 0..255, green :: 0..255, blue :: 0..255, alpha :: float)\n           | hsl(hue :: 0..360, saturation :: float, lightness :: float)\n           | hsla(hue :: 0..360, saturation :: float, lightness :: float, alpha :: float)\nend\n```\n\n### Example: Adding Recursive Type Specifications\n\n```elixir\ndefmodule IntegerTree do\n  import ExUnion\n\n  # You can also use `t` instead of `union` if you prefer\n  defunion leaf | node(integer :: integer, left :: union, right :: union)\nend\n```\n\nIf necessary you can ever refer to individual cases of the union.\nLet's revisit the `Color` example for above and how we can use recursive types to reuse the `rbg` and `hsl` definitions:\n\n```elixir\ndefmodule Color do\n  import ExUnion\n\n  defunion hex(string :: String.t)\n           | rgb(red :: 0..255, green :: 0..255, blue :: 0..255)\n           | rgba(rgb :: union_rgb, alpha :: float)\n           | hsl(hue :: 0..360, saturation :: float, lightness :: float)\n           | hsla(hsl :: union_hsl, alpha :: float)\nend\n```\n\n### Example: If you'd write all this by hand\n\nTo 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.\nFor this we'll use the `Maybe` example from earlier again.\n\n```elixir\ndefmodule Maybe do\n  @type t :: union\n  @type union :: Maybe.None.t() | Maybe.Some.t()\n  @type union_some :: Maybe.Some.t()\n  @type union_none :: Maybe.None.t()\n\n  defmodule Some do\n    @type t :: %__MODULE__{value: any}\n    defstruct [:value]\n\n    @spec new(fields :: %{value: any}) :: t\n    def new(fields) when is_map(fields) and :erlang.is_map_key(:value, fields) do\n      struct!(__MODULE__, fields)\n    end\n\n    @spec new(fields :: [value: any]) :: t\n    def new([{field, _} | _] = fields) when field in [:value] do\n      struct!(__MODULE__, fields)\n    end\n\n    @spec new(value :: any) :: t\n    def new(value) do\n      %__MODULE__{value: value}\n    end\n  end\n\n  defmodule None do\n    @type t :: %__MODULE__{}\n    defstruct []\n\n    @spec new() :: t\n    def new() do\n      %__MODULE__{}\n    end\n  end\n\n  defdelegate some(value), to: Maybe.Some, as: :new\n  defdelegate none(), to: Maybe.None, as: :new\n\n  defguard is_maybe(value)\n           when is_map(value) and :erlang.is_map_key(:__struct__, value) and\n                  :erlang.map_get(:__struct__, value) in [Some, None]\nend\n```\n\nOut of a single line of `defunion some(value) | none` `ExUnion` generated over 30 lines of code.\nAnd while the specifics of the generated code are opinionated in places, they do have a lot lower information density than the `defunion` line.\n\n## Comparison\n\n`ExUnion` can be compared to a number of other libraries.\n\n### [`Algae`][elixir:algae]\n\n[`Algae`][elixir:algae] offers for \"algebraic data types for Elixir\".\n\nSome people might prefer that, and that's perfectly fine!\nI 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.\n\nNot everybody is familiar with \"algebraic data types\" and arguably not everybody needs to be!\nBut on the other hand there's a lot of goodness in the tools they bring to the table.\n\n[`Algea`][elixir:algae] also offers its own flavor of tagged unions (or rather sum types) but also with more than that.\n`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.\n\nAt 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.\nOr maybe `ExUnion` is all you need and that would be fine too.\n\n### [`Ok`][elixir:ok] or [`Wormhole`][elixir:wormhole]\n\n[`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}`.\nBut they do only that.\n\nIf you want more tools to deal with `{:ok, value}` and `{:error, reason}` tuples, then they are great libraries.\nBut if you want additional tools to model similar tagged unions, then these libraries don't help you.\n\n`ExUnion` doesn't pretend to help you with `{:ok, value}` / `{:error, reason}`.\nThis isn't the motivation behind the project.\nIt does however give you more power to escape the limits of using tagged tuples to model unions.\n\n## Roadmap\n\n- [ ] Figure out a way to derive protocol implementations for union structs (e.g. for `Jason`)\n\n## Contributing\n\nContributions are always welcome but please read [our contribution guidelines](./CONTRIBUTING.md) before doing so.\n\n[elixir:algae]: https://github.com/witchcrafters/algae\n[elixir:ok]: https://github.com/CrowdHailer/OK\n[elixir:witchcraft]: https://github.com/witchcrafters/witchcraft\n[elixir:wormhole]: https://github.com/renderedtext/wormhole\n[hex]: https://hex.pm/packages/ex_union\n[hexdocs]: https://hexdocs.pm/ex_union\n","project_url":"https://awesome.ecosyste.ms/api/v1/projects/github.com%2Falexocode%2Fex_union","html_url":"https://awesome.ecosyste.ms/projects/github.com%2Falexocode%2Fex_union","lists_url":"https://awesome.ecosyste.ms/api/v1/projects/github.com%2Falexocode%2Fex_union/lists"}