Or: booleans can be very ambiguous without context, as in:
A Prolog programmer is delivering a baby.
The mother asks, “Is it a boy or a girl?”
and the Prolog programmer says: “Yes”.
(if you like this kind of jokes, follow Raphael Borun Das Gupta’s suggestion and check Sydney Padua’s Mr. Boole Comes to Tea out.)
Consider this C# function:
interface IFiltersOut
{
T[] FilterOut<T>(Func<T, bool> predicate, T[] collection);
}
or, equivalently, in F#:
type filterOut<'a> = ('a -> bool) -> ('a array)
It gets the elements of an array and, depending on the result of
predicate
, it keeps or it drops them.
Does a true
in predicate
mean to drop it or to keep it?
Good question!
This ambiguity is the manifestation of Boolean Blindness, a term that
I think was coined by Professor Dan Licata.
Boolean Blindness is a smell occurring whenever a function operating
on a boolean value forgets — or erases — the information
about what that boolean was meant to represent.
Let’s go straight to resolution. Here is a less ambiguous implementation:
enum Keep
{
Keep,
Drop
}
interface IFiltersOut
{
T[] FilterOut<T>(Func<T, Keep> predicate, T[] collection);
}
All the ambiguity is gone. F# would allow a stronger typed implementation:
type Keep = Keep | Drop
type filterOut<'a> = ('a -> Keep) -> ('a array)
It’s just unfortunate that C# does not support discriminated union types natively. We could compensate this lack with a slightly more verbose:
abstract record KeepOrDrop;
record Keep : KeepOrDrop;
record Drop : KeepOrDrop;
interface IFiltersOut
{
T[] FilterOut<T>(Func<T, KeepOrDrop> predicate, T[] collection);
}
Keep | Drop
conveys the same information of a boolean, plus a
bit of domain context.
The filter function is the canonical example when talking about Boolean Blindless. Here are some other poorly designed signatures:
void SellAlcohol(
User user,
Func<User, bool> checkAge);
bool ActivateFeature(
Account account,
Func<FileInfo, bool> readSettings);
decimal CalculateTotal(
Order order,
bool discountStatus);
Which meaning those bool
values have is absolutely arbitrary, at the
complete discretion of the implementor, and totally opaque to its
clients. There is no good or bad choice. It is just an ambiguity,
often clarified with comments, conventions or diligently selected
variable names. Or left there to our damage.
Here are the same signatures, with an attempt to clarify the ambiguity at the type level:
void SellAlcohol(
User user,
Func<User, IsAdult> checkAge);
bool ActivateFeature(
Account account,
Func<FileInfo, FeatureIsActive> readSettings);
decimal CalculateTotal(
Order order,
DiscountsEnabled discountStatus);
One could think that Boolean Blindness can be equally (and more easily) addressed assigning meaningful names to variables. Indeed, properly naming things helps. It’s not coincidental that Boolean Blindness belongs to the same family of Uncommunicative Name and Magic Number, which are about naming things.
Yet, resolving Boolean Blindness at type level ensures an enforcement by the compiler which is in general preferrable than relying solely on discipline. My take on this is that with statically and strongly typed languages, the more we leverage the compiler, the greater the benefit we obtain in the long run.
As we saw in Type Cardinality, types having the
same number of inhabitants are equivalent: there always exist
isomorphisms mapping one type onto the other.
This means that, ideally, we should be able to use Keep | Drop
wherever we used to have bool
.
This is generally true, although with a little caveat: most of the
languages treat booleans in a special way, reserving if/then/else
keywords to it. bool
is priviledged if compared with any custom type
you wish to introduce. That’s an unfair perk.
But not everything is lost. Pattern matching is the the way to go. You
can always build a mapping from your custom types to bool
with:
var keepIt = predicate(element) switch
{
Drop => false,
Keep => true
};
or use pattern matching directly in your expressions:
class FiltersOut : IFiltersOut
{
T[] IFiltersOut.FilterOut<T>(Func<T, KeepOrDrop> predicate, T[] collection) =>
collection
.Where(element => predicate(element) is Keep)
.ToArray();
}
This approach is more scalable, more flexible and more expressive than just using booleans: it is always possible to add additional cases and have the compiler supervising the usage and its consistency everywhere, for free.
My personal take-away is: wherever you find the opportunity, try to give an emerging domain notion the dignity of a type, so that you can treat it as a thing. This is especially true with booleans.
In my experience, the domain experts:
true
and false
in the context of checking age;false
in the context of
reading a configuration file;bool?
.Therefore, it’s just a matter of acklowleging this and writing down:
type Age = Adult | Underage
type Feature = Enabled | Disabled
type AccountState = Active | NonActive | ToBeConfirmed
Having a type-level domain language always pays off. DDD aficionados would call this Ubiquitous Language. I call it Type-Driven Domain-Modelling. Each to their own.
Should I ever manage to design my personal programming language, I
would surely not include any if
statement. Instead, I would make
my best to have a pattern matching machinery as convenient as
if
. But don’t hold your breath: I cannot even write a proper parser
yet. ArialdoLang is unlikely to see the light anytime soon.
If you are intollerant to hair-splitting posts, you can safely stop here. Instead, if you want to be inspired by mathematical and phylosophical arguments, go read Boolean Blindness by Robert Harper, or my summary in the appendix of this post.
If you want to keep your feet on the ground, please: next time you happen to be in front of a yes/no, true/false, here/there, this/that situation, try to model it with a type.
Feel free to insult me if this does not work: I will not complain.
Happy programming!