Originally published at https://programming-elm.com on May 30, 2019.
In the previous post, we explored how boolean arguments obscure the intent of code. We replaced boolean arguments with custom type values to make code more explicit and maintainable.
In this post, 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.
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.
doYouKnowTheTime function accepts a
Person type and checks if the
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
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.
We can still call
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
Maybe type. What Dan means is that booleans only tell you if something is
Maybe type tells you if it’s present by giving it to you when
it’s available. Let’s rewrite our example with
We update the
time field to be
Maybe String. Then, we add a
function that returns
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.
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.
We import the
Time module and expose
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
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.
We introduce a
CurrentTime custom type with three constructors,
CurrentTime constructor wraps
Posix. We then
time field to be
CurrentTime. In the
currentTime function, we
handle all three constructors. The
CurrentTime branch stays the same as the
Just branch. The
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
What You Learned
In this post, 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. Try refactoring some of your own code to replace a boolean return
value with a more meaningful custom type to make your code more maintainable.