Booleans Are a Trap
katafrakt.me
katafrakt.me
>Wait—that last state doesn't make sense. With a real door, you can technically turn the key while it's open, but does that meaningfully change its state?
Yes, you can't close a door that is open but the lock is turned. So this state actually makes sense in the real world.
Or desired. Perhaps the user had the need to keep the door from shutting and didn't have a wedge at hand.
This stuff can be hard, and sometimes, you just have to refactor later on instead of trying to nail down every possibility for the model, despite trying to figure out likely issues in the near-term. In which case, the goal is to keep in mind that data models will eventually change over time, and consider how that knowledge might change what you're writing now to make that process less painful.
It doesn't. The data is correctly representing the state of the system. Your "bug" assumes a lot of things about the environment, any of which may or may not be true depending on the specifics where the system is running.
> Or perhaps the sensor you use to determine the door's state (open or closed) is positioned on the hinge side in such a way that it's triggered when the door is kept open with the deadlock extended.
Again, an implementation problem. The sensor was installed in a way that's incompatible with the underlying system. Can it be fixed by changing the data model? Unlikely. In fact, I don't think it can be fixed at all. If the sensor lies, there's nothing the underlying system or the data model can do about it, especially if it's a binary sensor like the one described.
If the deadbolt was installed in such a way that it never crossed into the door frame but rather only moved inside the actual door, would you consider it a bug in the lock, the deadbolt, or simply bad installation?
Which means that the non buggy model that best reflect reality, and covers all possible scenarios that will happen in production is the two boolean one, not the enum.
So one could say that the article's fundamental flaw is not explaining what is the purpose of the model, therefore we cannot assess if it is capturing the relevant aspects to solve the problem.
Perhaps a better lesson is to not be overzealous when paring down your logical model.
This is a constant source of bugs in software: developer making a "reasonable" judgement that gets crushed by reality.
The core idea is true, though. Enumeration types, set operations and pattern matching are must-have features in any PL.
That depends. If it's a deadbolt, you're correct. If it's the lock in a doorknob, you probably can. This is typically how I lock my front door when I leave, I lock it then close it. Cars doors can also generally be locked then closed.
The real problem here is the same tension of typed vs untyped programs, applied to data constraints more specific than types. In an untyped language/database/etc, the expectation of isOpen could be updated to include an additional valid state of String("locked") as a third state - ugly but semantically correct (and of course now you have possible logic errors at every test of isLocked based on whether conditionals were properly created based on more than 2 boolean states (and possible how the language shortcuts "naked boolean" conditions)).
In an environment with the ability to express constraints on the data, the state (isOpen=true, isLocked=true) could be prohibited - a straightforward solution that requires some deliberate data modeling work.
[0] correctly used, as in the author's door example when pertaining to a door that automatically unlocks when going open->closed (which is actually relatively rare! so if this had been an instance of modeling the real world, I question whether it would have been actually correct!). Meanwhile the author's PremiumFeature example is really just using an enum to create a booleans in a different form, and doesn't actually support their thesis.
It will change how you write code.
Or put differently, don't let language or database affordances dictate your domain models.
There are many situations where either your own abstractions or the abstractions of the language sort of "project back" into your domain model and "suggest" certain features to implement or special cases to handle.
E.g. if you modeled some state as a bunch of boolean (or enum!) fields, then the language suggests that every combination of those field values should map to some meaningful real-world situation. But there is no real reason to assume this! Sometimes one flag only makes sense in the context of another flag, sometimes a specific combination of flags could theoretically occur, but is uninteresting as a business usecase.
The same can happen with self-designed abstractions and interfaces: If "everything" in your system is a CRUD entity, then there will be some temptation to think of some meaningful "update" or "delete" operation for every entity - even if no one actually asked for those features.
In those situations, it's OK to declare certain combination of values as "invalid" (e.g. if isOpen is true, the isLocked must be false) or as "equivalent" (e.g. if isOpen is true, then isLocked is ignored).
If we want to know the Door object's state, that's another set of messages.
If the door is open and locked, we don't know what's supposed to happen until we reference the business rules, which is what a lot of people on here are alluding to. Maybe we have a deadbolt, but maybe it's fine to assume that the user is going to unlock the door if necessary before closing it, and that the closed door is now in an unlocked state until the "lock" message is sent.
However this is implemented, our tests will only be on the Door object's interface, and we'll be testing the business rules.
Languages in the ML family solved this decades ago. But in the last 30 years this didn’t go mainstream.
It's started to in the last few years (last decade?). First OO languages started getting type-safe enums, which still suck but at least aren't as bad as C-style enums, then most recent languages got either actual sum types or something more or equivalent (type-safe unions, sealed classes / interfaces).
Rust is mainstream.
- boolean
- nullable boolean
- nullable datetime/timestamp (e.g. closed_at)
- status 'enum'/string/code
- combinations of the above isFavorite = true|false
This was originally meant to indicate an item would be pinned to the end users dashboard. Turns out that over time the business unit wanted to allow users to pin items to more then just a dashboard. If we had known better we would have used a set like favoriteLocations = Set<Locations>
but because this data was modeled across user devices in firmware and not just a central db, we were kinda stuck with adding more booleans for more locations to pin an item.Furthermore, you didn't need to keep using booleans. You could run a script that reads the boolean and update the new field in the row, and transition your code to use that.
For the general case, I think this may have been the inspiration for swift having enums with associated values, which in the author's toy example would have been
enum DoorState {
case open
case closed(Bool)
}And perhaps to the title: Misusing booleans is a trap.
And is this not the cruz of the engineering Goldilocks problem? We need to solve the problem at hand, not the one we might have in a year.
There is a good argument to be made against over-engineering.
no, booleans do not map perfectly onto how computers work at a low level: what happens when you add two booleans together?
C handles booleans how computers work at a low level.
Now you might say, well, adding two booleans together is an xor for that bit, and an and for the carry bit... would that were true, but we don't have access to that part of the ALU, it is already configured to be a full adder.
the only parts of the lowlevel bitwise boolean/binary mix that C is missing (and should be added) is the overflow flag and the carry flag
Start with the easy, and change it when you need it.
If you use the easy boolean, but your code requires a different class for each layer with that boolean (I'm looking at you DDD) if you need to change to an enum...good luck touching everything.
If you start with the complex enum, thinking that the boolean will need to change, then you'll realize that you don't need an enum or a boolean, but a string, and all that extra complexity is now in the way.
Boolean or enum, doesn't really matter, but make it so that you can change it as easily as possible, if needed