Jeremy Fairbank
Jun 20 · 13 min read

Originally published as a series at https://programming-elm.com and on Medium. Part 1 covers boolean arguments. Part 2 covers boolean return values. Part 3 covers boolean properties.


Back in September 2017, I presented the talk “Solving the Boolean Identity Crisis” at ElmConf. The talk highlights the downsides of using booleans in Elm code and offers ways to write clearer, more maintainable code. This post shares what I explored in that presentation. You can preview what’s to come by watching my talk on YouTube.

In this post, you will see how boolean function arguments obscure the intent of code. You will learn how to replace boolean arguments with Elm’s custom types to write more understandable code.

Next, you will discover that boolean return values cause a problem known as boolean blindness. Boolean blindness can create accidental bugs in if-else expressions that the compiler can’t prevent. You will learn how to replace boolean return values with custom types to eliminate boolean blindness and leverage the compiler for safer code.

Finally, you will see that boolean properties in Elm records open the door to invalid state configurations. Boolean properties require more complex conditional code and more tests to prevent bugs. You will learn that custom types–seeing a pattern here–eliminate the need for complex code and tests by harnessing the power of the compiler to prevent invalid state configurations.

The Problem with Boolean Arguments

Look at this function call to understand the problem with boolean arguments.

bookFlight "OGG" True

We pass in a string argument "OGG" and a boolean argument True to a bookFlight function. If you encountered this in an Elm codebase, you might wonder what the boolean argument does.

Boolean arguments hide the intent of code. We don’t know the significance of the True value here without looking up the definition of bookFlight. The boolean argument makes this code harder to understand, especially as a newcomer learning the codebase.

Looking up the definition, we find this. (I use ... to signify irrelevant code.)

bookFlight : String -> Bool -> Cmd Msg
bookFlight airport isPremium =
if isPremium then
...
else
...

The boolean argument is called isPremium, so it means the booking customer has a premium status. We use an if-else expression to branch on isPremium. If isPremium is False, we're not certain what status this customer has. We have to assume that the customer has a "regular" status because the code makes that implicit. We've lost the explicit intent of this code by using a boolean argument.

This code will present future problems if we need more than one customer status. For example, let’s say we need to introduce a new economy status. We could introduce another boolean argument called isRegular.

bookFlight : String -> Bool -> Bool -> Cmd Msg
bookFlight airport isPremium isRegular =
if isPremium then
...
else if isRegular then
...
else
...

After checking if isPremium is True, we check if isRegular True. Otherwise, the implicit customer status is economy.

Now, function calls will look like this.

bookFlight "OGG" True False

That’s even more confusing. We could easily mix up the order of the boolean arguments too and accidentally book a customer with the wrong status. Also, we could easily pass in two True arguments. A customer can't have both premium and regular status. We have to let the first boolean argument isPremium take precedence in the if-else expression to deal with this invalid argument permutation.

Show Intent

We can clean up the bookFlight function by replacing the boolean arguments with an Elm custom type. Instead of hiding statuses behind boolean values, let's make them explicit. We can easily encode each type of status like so.

type CustomerStatus
= Premium
| Regular
| Economy

We add a CustomerStatus custom type with three values, or constructors. Each value perfectly encodes each status, Premium, Regular, and Economy.

We can update the bookFlight function like so.

bookFlight : String -> CustomerStatus -> Cmd Msg
bookFlight airport customerStatus =
case customerStatus of
Premium ->
...
Regular ->
...
Economy ->
...

The bookFlight function makes it clear how to handle each customer status without implicit if-else branching. Additionally, the compiler ensures we handle each status. In the previous version of bookFlight with two boolean arguments, nothing would prevent us from accidentally forgetting to handle the else if isRegular branch. The compiler would accept this code.

bookFlight airport isPremium isRegular =
if isPremium then
...
else
...

If we forgot the Regular branch in the version with the CustomerStatus type, the code would not compile.

This code:

bookFlight airport customerStatus =
case customerStatus of
Premium ->
...
Economy ->
...

Would result in this compiler error:

This `case` does not have branches for all possibilities:|>    case customerStatus of 
|> Premium ->
|> ...
|>
|> Economy ->
|> ...
Missing possibilities include:Regular

Custom types provide compiler safety along with explicit code. Now calls to bookFlight declare the intent of code because we pass in the CustomerStatus directly.

bookFlight "OGG" Premium

If we ran into the above function, we would more easily understand what’s happening. We’re booking a flight for a premium customer. We’ve made the code clearer and more maintainable.

The Problem with Boolean Return Values

In my talk, I share a tale from a lecture by Dan Licata, a professor at Wesleyan University.

Sometimes, when I’m walking down the street, someone will ask me “do you know what time it is?” If I feel like being a literalist, I’ll say “yes.” Then they roll their eyes and say “okay, [tell] me what time it is!” The downside of this is that they might get used to demanding the time, and start demanding it of people who don’t even know it. It’s better to ask “do you know what time is it, and if so, please tell me?”. [T]hat’s what “what time is it?” usually means. This way, you get the information you were after, when it’s available.

If we translate this into code, it might look like this.

type alias Person =
{ time : String }
doYouKnowTheTime : Person -> Boolean
doYouKnowTheTime person =
person.time /= ""
tellMeTheTime : Person -> String
tellMeTheTime person =
person.time
currentTime : Person -> String
currentTime person =
if doYouKnowTheTime person then
tellMeTheTime person
else
"Does anybody really know what time it is?"

The doYouKnowTheTime function accepts a Person type and checks if the time field isn't the empty string. Then, we branch on a call to doYouKnowTheTime inside the currentTime function. If it returns True, then we call tellMeTheTime to return the value of person.time. Otherwise, we return a default time.

This code may look fine but it suffers from a couple of problems.

First, as Dan rightly points out, people could demand time of others that don’t have it. Nothing stops us from writing this code.

currentTime person =
if doYouKnowTheTime person then
tellMeTheTime person
else
tellMeTheTime person -- returns empty string

We can still call tellMeTheTime when person.time is the empty string. This would likely cause a bug.

Second, the fact that we can cause the previous situation surfaces a data-modeling code smell. Strings notoriously cause trouble because any string is valid according to the type system. The compiler can’t enforce that a given string is not empty. This is a weak substitute for a more meaningful data type.

We want to give the compiler better type information so it can constrain this code to only access the time when it’s truly available. Let’s explore how to make this code clearer and safer.

Fix the Boolean Blindness

The first problem stems from boolean blindness. When you reduce information to a boolean, you lose that information easily. The information that boolean carries is only known inside the if check. As soon as you branch into the body of the if-else expression, you become blind to the original information that got you there. Because that boolean loses information, you must backtrack to recover it when you need it again.

Dan offers this solution to boolean blindness, “boolean tests let you look, options let you see.”

Dan is referring to the option type in ML. In Elm, we call it the Maybe type. What Dan means is that booleans only tell you if something is present. The Maybe type tells you if it's present by giving it to you when it's available. Let's rewrite our example with Maybe String.

type alias Person =
{ time : Maybe String }
whatTimeIsIt : Person -> Maybe String
whatTimeIsIt person =
person.time
currentTime : Person -> String
currentTime person =
case whatTimeIsIt person of
Just time ->
time
Nothing ->
"Does anybody really know what time it is?"

We update the time field to be Maybe String. Then, we add a whatTimeIsIt function that returns person.time. Inside currentTime we now call whatTimeIsIt and pattern match on the result. If the person has the time, then we immediately have access to it inside Just. No need to first check with an if-else expression. If the person doesn't have the time, i.e. Nothing, then we return our default.

We can’t accidentally access the time if it’s not present because the compiler will enforce the Maybe type constraint.

We still have a problem, though. The time inside Just could be the empty string, which is an invalid time. Let's fix that next.

Use Time.Posix

We need a better type for encoding the time to avoid the empty string. Luckily, Elm has a package for working with time called elm/time. It offers a Posix type to represent Unix time, or the amount of time that has passed since midnight UTC on January 1, 1970. We can use the Posix type and then convert it to a formatted time when needed.

import Time exposing (Posix, toHour, toMinute, utc)type alias Person =
{ time : Maybe Posix }
whatTimeIsIt : Person -> Maybe Posix
whatTimeIsIt person =
person.time
currentTime : Person -> String
currentTime person =
case whatTimeIsIt person of
Just time ->
String.fromInt (toHour utc time)
++ ":"
++ String.fromInt (toMinute utc time)
Nothing ->
"Does anybody really know what time it is?"

We import the Time module and expose Posix, toHour, toMinute, and utc. We change the time field to Maybe Posix and update the type annotation for whatTimeIsIt. Inside the Just branch of currentTime, we now know we have a valid time thanks to the Posix type. We use the toHour and toMinute functions along with String.fromInt and the utc time zone to build a formatted string time.

This is great. Because of static types, the compiler will enforce our code to only access a valid time when it exists.

We could go one step further to improve this code. If a person doesn’t have the time, then it’s Nothing. But, that doesn't explain why the person doesn't have time. We can replace Maybe with our own custom type.

type CurrentTime
= CurrentTime Posix
| NoWatch
| InAHurry
type alias Person =
{ time : CurrentTime }
currentTime : Person -> String
currentTime person =
case whatTimeIsIt person of
CurrentTime time ->
String.fromInt (toHour utc time)
++ ":"
++ String.fromInt (toMinute utc time)
NoWatch ->
"I don't have the time."
InAHurry ->
"Sorry, I'm in a hurry."

We introduce a CurrentTime custom type with three constructors, CurrentTime, NoWatch, and InAHurry. The CurrentTime constructor wraps Posix. We then change the time field to be CurrentTime. In the currentTime function, we handle all three constructors. The CurrentTime branch stays the same as the previous Just branch. The NoWatch and InAHurry branches each return a string that describes why the person doesn't have the time.

Now, we have made the code more precise about why a person doesn’t have the time and have encoded better business domain rules into the code with custom types. Plus, we still have the compiler to ensure we can only access a valid time in the CurrentTime branch.

The Problem with Boolean Properties

I begin my talk with a problem I encountered while building applications with Redux and React. When fetching data from a server, I would track the state of fetching that data with multiple boolean properties. Unfortunately, I brought that pattern over to the Elm applications I built.

For example, let’s say we’re building a application for tracking rescue dogs. We need to fetch a dog from the server. Initially, we wouldn’t have a dog, so we would likely have a model like this.

type alias Model =
{ dog : Maybe Dog }

That seems reasonable so far. Next, we want to display a loading spinner while we fetch the dog from a server. So, we could add a fetching property to the model.

type alias Model =
{ dog : Maybe Dog
, fetching : Bool
}

When fetching is True, we will display the spinner. When fetching is False, we will display nothing.

Once we have the dog, fetching should be False, but we want to display the dog. We could add a success boolean property to indicate we have the dog.

type alias Model =
{ dog : Maybe Dog
, fetching : Bool
, success : Bool
}

Now, if success is True, we display the dog. Otherwise, if it's False and fetching is False, then we're back in a "ready to fetch" state and should display nothing. (Alternatively, we could look at a combination of fetching and if dog is Just or Nothing to decide what state we're in.)

All seems well, but the dog could not exist on the server or we could encounter other server errors. We need to know if the request failed and handle any errors appropriately. Well, we could add an error boolean property along with an errorMessage property.

type alias Model =
{ dog : Maybe Dog
, fetching : Bool
, success : Bool
, error : Bool
, errorMessage : String
}

If error is True, then we can display the errorMessage. Otherwise, we'll need to examine the other boolean properties to determine what to do.

If we were to handle this in the view function, it might look like this.

view : Model -> Html Msg
view model =
if model.error then
viewError model.errorMessage
else if model.fetching then
viewSpinner
else if model.success then
viewDog model.dog
else
viewSearchForm

The view function has a couple of issues.

  1. It suffers from boolean blindness. We depend on certain boolean properties to be true before attempting to access data. Nothing stops us from accessing data in other branches such asmodel.dog or model.errorMessage. (Granted, if we tried to accessmodel.dog, we'd still have the safety of Maybe.)
  2. It requires more thorough automated testing to ensure we handle all cases properly. We could leave out all the else if branches and the code would still compile even if it was incorrect.

Also, our model can arrive at incorrect configurations like this.

{ dog = Just { name = "Tucker" }
, fetching = True
, success = True
, error = True
, errorMessage = "Uh oh!"
}

All boolean properties are true, we have a dog, and we have an errorMessage. We're hard-pressed to determine what state we're really in. We have no choice but to depend on the arbitrary ordering of the if-else conditionals in view to make that decision. Of course, we'll need a strong test suite to ensure we can't configure the model like this.

Prevent Invalid State

I finally realized the problem with how I represented my data. I thought the states of fetching data (ready, fetching, success, and error) were separate from one another. Really, they are different state values of the same overall state. That sounds like a state machine.

A state machine can only be in one state value at a time. My record representation forbid that by letting multiple state values be True. Elm has an awesome type system and compiler. We should leverage them as much as possible to prevent invalid state configurations by essentially creating a state machine.

We could introduce a new custom type.

type RemoteDoggo
= Ready
| Fetching
| Success Dog
| Error String

The RemoteDoggo type has four constructors that map to each possible state, Ready, Fetching, Success, and Error. We wrap a Dog with the Success constructor and wrap a String error message with the Error constructor. Then, we can update the model to look like this.

type alias Model =
{ dog : RemoteDoggo }

We remove all but the dog property and change the dog property to the RemoteDoggo type. We can now transform the view function into this.

view model =
case model.dog of
Ready ->
viewSearchForm
Fetching ->
viewSpinner
Success dog ->
viewDog dog
Error error ->
viewError error

Instead of worrying about the order of boolean properties, we pattern match on the dog property with a case expression. We map each constructor to the appropriate view helper function.

Our code has gained a few benefits here by this change.

  1. The code makes the states explicit with the RemoteDoggo type.
  2. We eliminated boolean blindness. We can only access the dog in Success and the error message inError.
  3. We have compiler enforced UI states. If we forget to handle one of the RemoteDoggo values, then our code won't compile.

This code:

view model =
case model.dog of
Ready ->
viewSearchForm
Fetching ->
viewSpinner
Success dog ->
viewDog dog
-- forgetting to handle errors

Will produce this compiler error:

This `case` does not have branches for all possibilities:|>     case model.dog of
|> Ready ->
|> viewSearchForm
|>
|> Fetching ->
|> viewSpinner
|>
|> Success dog ->
|> viewDog dog
Missing possibilities include:Error _

Now our code is clearer and safer thanks to custom types.

What You Learned

In this post, you learned how boolean arguments can make code confusing and unmaintainable by hiding the intent of code. You saw how replacing boolean arguments with custom type values created better, safer code.

Then, you learned that boolean return values cause boolean blindness. You saw that boolean blindness can lead to human error by letting code access data in incorrect places. You discovered that built-in custom types such as Maybe or your own custom type let you test and access the presence of data. Additionally, the compiler ensures you access data only when it's truly available.

Finally, you learned that boolean properties can cause invalid state configurations, which create bugs that the compiler can’t catch. Boolean properties lead to complex if-else expressions with arbitrary ordering that are hard to follow. You saw that by reducing the boolean properties down to one property with a custom type you can write more explicit code with compiler safety.

Try these techniques out on your own Elm project. Find boolean arguments, boolean return values, and boolean properties that you can replace with more meaningful custom types to make your code more explicit, safe, and maintainable.

Further Resources

For more info on how to use Elm’s type system to prevent invalid state configurations, watch Richard Feldman’s talk Making Impossible States Impossible.

For a more general type similar to RemoteDoggo, check out the krisajenkins/remotedata package.

To learn more about how to build Elm applications effectively, grab a copy of my book Programming Elm from The Pragmatic Programmers.

The Startup

Medium's largest active publication, followed by +514K people. Follow to join our community.

Jeremy Fairbank

Written by

Author of Programming Elm. Speaker. Software Engineer and Consultant at Test Double.

The Startup

Medium's largest active publication, followed by +514K people. Follow to join our community.

Welcome to a place where words matter. On Medium, smart voices and original ideas take center stage - with no ads in sight. Watch
Follow all the topics you care about, and we’ll deliver the best stories for you to your homepage and inbox. Explore
Get unlimited access to the best stories on Medium — and support writers while you’re at it. Just $5/month. Upgrade