Booleans and Enums

Joël Quenneville

Booleans are one of the first data types new programmers learn and with good reason: having only two states makes them one of the simplest. Surely something so simple can’t be abused?

It turns out it’s easier than you’d think to make a mess with Booleans. Languages with first-class support for enums like C, Swift, Rust, and TypeScript provide the tools for some really nice refactorings.

Note that enums go by a few alternate names such as sum types and union types. They’re also a subset of a greater concept called algebraic data types (ADT) so you may hear them referred to as this.

The refactorings below can be applied in any language. I’ll be using Elm in the examples for its first-class support of enums (which it refers to as union types) and strict, friendly compiler.

Three-state Boolean

Booleans represent one of two states: True or False. If we allow a Boolean value to be null (or in Elm’s case Maybe), then there are three possible states. This is the three-state Boolean problem.

Let’s say we’re building a checkout system that allows restaurants to take orders online. When we start there are two types of orders: delivery or pickup. Two states, that sounds like a Boolean! But what about the case when the user hasn’t selected an option yet? Sounds like the classic use of a Maybe. But now, our option has three states, not two.

type alias Order =
  { id : Int
  , delivery : Maybe Bool
  }

With three states, we’ve outgrown our Boolean. Instead of using the primitive Maybe Bool, we can construct a union type that represents our three states:

type DeliveryType = Delivery | Pickup | NoneSelected

type alias Order =
  { id : Int
  , deliveryType : DeliveryType
  }

This approach has a few advantages:

  • Extensible it’s easy to add a fourth state.
  • Readable it’s more obvious what a delivery of type NoneSelected is than a delivery of type Nothing.

Double Boolean

Another common situation is to have two dependent Booleans. Let’s say our checkout system allows orders to be fulfilled via third-party delivery services such as DoorDash. An order is either fulfilled via a third party or it isn’t. Sounds like another Boolean!

type alias Order =
  { id : Int
  , delivery : Bool
  , thirdParty : Bool
  }

Take a closer look and you realize that the two Booleans are dependent. What we now have is a single state that is expressed as a combination of two Boolean flags. We have a clunky system for expressing 4 states. Even worse, some of the states don’t even make sense.

What is a non-delivery order fulfilled via third party???

{ id = 1
, delivery = False
, thirdParty = True
}

We can express our four states using a union type instead:

type DeliveryType
  = Delivery
  | Pickup
  | ThirdPartyDelivery
  | NotSelected

type alias Order =
  { id : Int
  , deliveryType : DeliveryType
  }

As previously, this has a few advantages:

  • Extensible it’s easy to add a fifth state.
  • Readable calculating the state based on dependant Booleans is clunky.
  • Correct no invalid states permitted.

Boolean Flags

You’re building a role-playing game as a side project because games are fun! You want players to interact with non-player characters (NPC). Not a problem. We already have a Character type. We just need to set the correct attributes on it.

The implementation is a single line but what does it even mean? Those are probably flags of some sort but who knows what they do. There’s got to be a better way.

npc : Character
npc =
  Character True True False

Turns out there is! Using union types, it’s pretty obvious what the attributes of this NPC are.

npc : Character
npc =
  Character Immortal Civilian Stationary

It is completely OK to define a union type with only two values.

type Mortality = Mortal | Immortal
type MilitaryStatus = Civilian | CityGuard
type Mobility = Stationary | Mobile

type alias Character =
  { mortality : Mortality
  , militaryStatus : MilitaryStatus
  , mobility : Mobility
  }

While it may feel like you’re just re-implementing Booleans, this approach has a few advantages:

  • Extensible it’s easy to add a third state.
  • Readable this technique adds a massive readability boost to your code.
  • Correct it is now impossible to accidentally pass the character’s mobility to a function when you really meant to pass mortality. The compiler now knows the difference between the two and can catch the error.

Why not strings

After seeing all this, you maybe thinking “Union types are cool and all but couldn’t I just use strings?”. There is a small difference between the two that makes a big impact.

Consider the following type:

type Mobility = Mobile | Stationary

How many different allowed values are there? Two.

Now consider the equivalent string.

type alias Mobility = String

How many allowed values are there? Infinite.

This is a big deal. If you’re writing a case statement for a mobility, you will need to handle all those infinite other invalid values you don’t care about with a catch-all. If you ever add a new mobility (say “Flying”), it will just silently be ignored by your case statement.

move : Int -> Character -> Character
move distance character =
  case character.mobility of
    "Mobile" ->
      { character | position = character.position + distance }

    "Stationary" ->
      character
    _ ->
      character

On the other hand, adding a new Flying value to the union type results in a compiler error that tells you all the places you need to change to accommodate the new mobility. Yay type safety!

-- MISSING PATTERNS ------------------------------------------------------------

This `case` does not have branches for all possibilities.

11|>    case character.mobility of
12|>      Mobile ->
13|>        { character | position = character.position + distance }
14|>
15|>      Stationary ->
16|>        character

You need to account for the following values:

    Flying

Because there are infinite strings, any string is an allowed mobility including typos. The compiler can’t help you here.

npc : Character
npc =
  Character "Mobil" 55

On the other hand, because there are only a limited set of values in the union type, the compiler can easily catch typos and even suggest fixes:

-- NAMING ERROR ----------------------------------------------------------------

Cannot find variable `Mobil`

20|   Character Mobil 55
                ^^^^^
Maybe you want one of the following?

    Mobile

Primitive obsession

Booleans are some of the first data structures we are introduced to when learning to program. It seems so sensible to model values that have two-states as Booleans. This often leads to trouble because most “two-state” values in the real world aren’t as binary as you might assume.

Once you have two states, you will likely be adding a third or more at some point even if just an “empty” or “null” state. Unless you are dealing with states of truth, Booleans are probably not the best way to model your problem.

Instead of using a primitive, build your own union type to express your states. Not only does this better allow you to model your domain, but it also improves readability and adds better type safety. Win all round!