---
title: Modeling with Union Types
teaser: Clean up Maybe by modeling multiple shapes of data with union types.
tags: elm,web
author: Joël Quenneville
published_on: 2017-10-13
---

In a [previous article], I showed how we can refactor messy Boolean code using
**enums** (AKA **union types**).

Many languages turbo charge their enums, allowing them to have parameters. These
are often called **tagged unions** or [**Algebraic Data Types (ADTs)**]. Tagged
unions are a killer language feature as they allow you to expressively model
problem domains and avoid some of the pitfalls of relying entirely on
primitives.

I'm using [Elm] for the examples here due to it's first-class support of tagged
unions and strict, helpful compiler.

[previous article]: https://thoughtbot.com/blog/booleans-and-enums
[Elm]: http://elm-lang.org/
[**Algebraic Data Types (ADTs)**]: http://blog.jenkster.com/2016/06/functional-mumbo-jumbo-adts.html

## Multi-Maybe

Unlike languages like C, JavaScript, and Ruby, Elm has no [`null` / `nil`]. You
can explicitly tag a value as possibly not present with [`Maybe`][`Maybe`]
(often called `Option` in other languages like Swift). Instead of "everything
_could_ be null unless you've explicitly checked it", you can work on the
assumption that "everything is guaranteed present unless it's wrapped in
`Maybe`.

This is incredibly useful but it's easy to overuse, particularly when coming
from a language based around `null`. In Elm, records where a lot of fields are
`Maybe` are a smell.

Take for example the following `Booking`:

```elm
type alias Booking =
  { date : Maybe Date
  , option : Maybe BookingOption
  , tickets : Maybe Ticket.Quantities
  }
```

A user first starts by selecting a date. A request is made to an API to fetch
options for that date. The user then selects an option and another API call is
made to see available tickets for that option.

Using `Maybe` here is problematic because it allows invalid combinations such as
a selected option without a date.  There are 8 (2^3) possible states that can be
represented with this `Booking` type  but only 4 are legitimate: an empty state
and the three phases of selection.

We can use a tagged union to represent our 4 legitimate states:

```elm
type alias Booking =
    { date : Date
    , option : BookingOption
    , tickets : Ticket.Quantities
    }

type BookingProcess
    = NotStarted
    | DateSelected Date
    | OptionSelected Date BookingOption
    | TicketsSelected Booking
```

[`null` / `nil`]: https://thoughtbot.com/blog/if-you-gaze-into-nil-nil-gazes-also-into-you
[`Maybe`]: https://thoughtbot.com/blog/maybe-thinking

## Nested Maybe

When coming from languages based around `null`, it's easy to turn to `Maybe`
whenever our data doesn't quite fit the "shape" of our structures.

Here we have a rental that may or may not have an option selected which may or
may not have a description. As it stands, our nested `Maybe` allows four states
(2^2) that aren't super obvious. It's also annoying to chain `Maybe`s in order
to access the description.

```elm
type alias Rental =
  { date : Date
  , option : Maybe RentalOption
  }

type alias RentalOption =
  { name : String
  , id : String
  , description : Maybe String
  }
```

From a business perspective there are three states:

1. No option selected
2. Option selected but not described
3. Option selected and described

We can easily turn these into code with a union type:

```elm
type RentalOption
  = NotSelected String String
  | NotDescribed String String
  | Complete String String String
```

## The Shape of Data

Hiding behind all the maybes we've been looking at is a larger problem: We're
trying to force data that can have multiple "shapes" into a single shape and
using `Maybe` whenever things don't quite fit.

Union types are great at solving this problem because they allow us to specify
multiple shapes for our data. For example, say we're modeling a deck of cards:

```elm
type alias Card =
  { suit : String
  , value : Int
  }
```

This allows us to create cards like:

```elm
Card "Hearts" 2
```

But it also allows us to create completely invalid cards like:

```elm
Card "Starfish" 1999
```

We can solve this problem by creating enums that list the valid suits and
values:

```elm
type Suit
  = Hearts
  | Spades
  | Diamonds
  | Clubs

type Value
  = Two
  | Three
  | Four
  | Five
  | Six
  | Seven
  | Eight
  | Nine
  | Ten
  | Jack
  | Queen
  | King
  | Ace

type alias Card =
  { suit : Suit
  , value : Value
  }
```

Great! Now our cards will always be valid.

So far, all cards have had the same "shape". But what about the Joker? It
doesn't really have a value or a suit. We could use `Maybe` to try and force it
into our existing shape. Perhaps

```elm
type alias Card =
  { suit : Maybe Suit
  , value : Maybe Value
  }
```

where `Card Nothing Nothing` is a Joker. We could get rid of one of the `Maybe`s
by making `Joker` either an suit type or a value but that also allows invalid
configurations like a "3 of Jokers" or a "Joker of Hearts". Trying to force a
Joker into our single card shape is not going to work.

Instead, we can use a union type to allow multiple shapes:

```elm
type Card
  = StandardCard Value Suit
  | Joker
```
