You Might Not Want a Boolean
We use booleans all the time, usually without considering if they’re the right tool for the job. They’re probably the one truly universal language feature. But a boolean isn’t always the best choice.
Types can make code easier to understand, improving clarity, and let the computer verify our work, improving correctness. Different types provide different levels of clarity and correctness. This is why the benefits of types can be hard to see if you’re new to them: you need to learn how to use them in order to get the benefits.
Booleans are universal, but don’t maximize clarity or correctness. By using a different type, we can often increase clarity or correctness in a meaningful way.
Clarity
Naming is important. A good name helps us understand code. Even if you’re the author, a name can clarify what a method or module should be. This is just as true for types as it is for any other part of an API.
Booleans have 3 names. In Swift, they’re:
Bool
(the type)true
(the affirmative)false
(the negative)
These names don’t mean anything. If you see Bool
or false
in a block of code, you need to look at some other name to see what it means.
let someView: UIView = …
// Hmm?
// 1. What does `false` mean here?
// 2. What does the return value indicate?
if someView.endEditing(false) {
…
} else {
…
}
// Answers:
// 1. Don't force the responder to resign
// 2. When true, the responder resigned
This is why the Swift API Design Guidelines suggest “compensating for weak type information” by using longer names that describe intent. e.g., by naming this method endEditing(force:)
instead.
But longer names aren’t perfect. Sometimes it’s hard to add a more descriptive name, especially with return values. (I can’t think of a better name for endEditing(_:)
to indicate the return value.) It’s also easy to accidentally invert boolean conditions, no matter how they’re named.
Using a different type can improve this.
Replacing a boolean is easy. Conceptually, a Bool
is an enum with two cases1. Any other enum with 2 cases and no associated values is isomorphic, so it can be used as a replacement.
In this case, defining a new enum for the return value makes the code much clearer:
enum EndEditingResult {
case resigned
case refused
}
let someView: UIView = …
switch someView.endEditing(false) {
case .resigned:
…
case .refused:
…
}
That’s the power of a good name. The code becomes clearer. Mistakes are less likely. And it only requires a few boring lines of code.
Correctness
Improving clarity can help with correctness, but there are deeper gains to be made.
Before programming languages had Optional
or Maybe
types, we had null pointers. When you wanted to use an object, you’d need to check (or know) whether the pointer was null.2 Checking whether a pointer was valid was error prone.
NSDate *date = …;
if (date != nil) {
NSLog("Found date: %@", date);
}
…
// Oops! `date` can be `nil` here. If it is, this will crash.
// Unfortunately, the Objective-C compiler won't help us.
Schedule *schedule = [[Schedule alloc] initWithDates:@[ date ]];
Swift’s Optional
type protects us from these mistakes. The compiler will insist that you provide evidence that date
isn’t nil. (Or you can use !
to tell the compiler “just trust me” and potentially crash.)
enum Optional<Wrapped> {
case some(Wrapped) // `Wrapped` is evidence of the some-ness
case none
}
This is an example of a more general technique: if something is valid only in the true
case or only in the false
case, you can provide evidence as part of an enum case—showing your work so the compiler can verify it.
There are many other examples of this using in any language with an Optional
or Maybe
type:
Arrays can be empty, so
array[0]
is only valid whenarray.isEmpty
isfalse
.array.first
returns anOptional
element, checking whether it’s empty and providing evidence that the first element exists if it’s not empty.Character
has aisHexDigit
property, but it also has ahexDigitValue
property that both checks whether it’s a hex digit and provides evidence of its hexiness if it is.
Optional
works well for cases where either true
or false
indicates that something is valid since one of Optional
’s cases has an associated value. If both true
and false
indicate that something is valid, you need an enum where both cases have an associated value, like Result
.
enum Result<Success, Failure> {
case success(Success) // `Success` is evidence of success
case failure(Failure) // `Failure` is evidence of failure
}
If null pointers are the canonical example for types like Optional
(enums with 2 cases and 1 associated value), then errors are the canonical example for Result
-like types.
NSError *error;
NSArray *array = [[NSArray alloc] initWithContentsOfURL:… error:error];
if (array) {
// it's only valid to use `array` here
} else {
// it's only valid to use `error` here
}
By replacing a boolean with an enum that carries additional information, we show our work, letting the compiler catch our mistakes.
Clarity and Correctness
These two shortcomings of booleans—lack of clarity and correctness—are known as boolean blindness. You can’t look at a boolean and know (1) what it means or (2) what operations are valid in response. You need more information. We can provide that information with better names or evidence, so we can see the meaning behind the values.
We can also combine these 2 techniques.
Boolean
’s 3 names (Bool, true, false) don’t have inherent meaning. Neither do Optional
’s (Optional, some, none). But Result
’s do: it combines both of these techniques (Result, success, failure).
There’s a commonly used type that’s equivalent to Result
, but doesn’t have meaningful names. It’s called Either
.
enum Either<Left, Right> {
case left(Left)
case right(Right)
}
Either
lets us write correct code by providing evidence for each case. But the names aren’t helpful, so it doesn’t make code any clearer. This is why I avoid Either
and hope it will never be added to the Swift standard library. I think writing equivalent custom types that have meaningful names is worth a little bit of boilerplate.
Type-Driven Design
These ideas aren’t limited to booleans. Meaningful names make code clearer and evidence proves correctness, no matter the size or shape of the type.
Providing evidence, in particular, is worth considering. Alexis King has written an excellent article about this: Parse, don’t validate. When you check array.isEmpty
, you’re validating that some condition is true, but not capturing that information. Instead of throwing away that information, you can save it, turning your validator into a parser.
This is the essence of what types can offer us. Showing your work lets the compiler verify it. In my experience, this has a transformative effect on a codebase. Inevitably, the types will begin showing you that certain codepaths are impossible, letting you remove cruft that’s hiding the core of your programs.
It takes time to learn how to use types well. Booleans are a good place to start.
-
Swift’s
Bool
type is implemented as a struct for historical reasons. ↩ -
Yes, Objective-C worked around this somewhat by having
[nil message] == nil
. But it was still a very real problem that led to crashes. ↩