Compile-time type-checked truth tables by Mark Seemann
With simple and easy-to-understand examples in F# and Haskell.
Eve Ragins recently published an article called Why you should use truth tables in your job. It's a good article. You should read it.
In it, she outlines how creating a Truth Table can help you smoke out edge cases or unclear requirements.
I agree, and it also beautifully explains why I find algebraic data types so useful.
With languages like F# or Haskell, this kind of modelling is part of the language, and you even get statically-typed compile-time checking that tells you whether you've handled all combinations.
Eve Ragins points out that there are other, socio-technical benefits from drawing up a truth table that you can, perhaps, print out, or otherwise share with non-technical stakeholders. Thus, the following is in no way meant as a full replacement, but rather as examples of how certain languages have affordances that enable you to think like this while programming.
F# #
I'm not going to go through Eve Ragins' blow-by-blow walkthrough, explaining how you construct a truth table. Rather, I'm just briefly going to show how simple it is to do the same in F#.
Most of the inputs in her example are Boolean values, which already exist in the language, but we need a type for the item status:
type ItemStatus = NotAvailable | Available | InUse
As is typical in F#, a type declaration is just a one-liner.
Now for something a little more interesting. In Eve Ragins' final table, there's a footnote that says that the dash/minus symbol indicates that the value is irrelevant. If you look a little closer, it turns out that the should_field_be_editable
value is irrelevant whenever the should_field_show
value is FALSE
.
So instead of a bool * bool
tuple, you really have a three-state type like this:
type FieldState = Hidden | ReadOnly | ReadWrite
It would probably have taken a few iterations to learn this if you'd jumped straight into pattern matching in F#, but since F# requires you to define types and functions before you can use them, I list the type now.
That's all you need to produce a truth table in F#:
let decide requiresApproval canUserApprove itemStatus = match requiresApproval, canUserApprove, itemStatus with | true, true, NotAvailable -> Hidden | false, true, NotAvailable -> Hidden | true, false, NotAvailable -> Hidden | false, false, NotAvailable -> Hidden | true, true, Available -> ReadWrite | false, true, Available -> Hidden | true, false, Available -> ReadOnly | false, false, Available -> Hidden | true, true, InUse -> ReadOnly | false, true, InUse -> Hidden | true, false, InUse -> ReadOnly | false, false, InUse -> Hidden
I've called the function decide
because it wasn't clear to me what else to call it.
What's so nice about F# pattern matching is that the compiler can tell if you've missed a combination. If you forget a combination, you get a helpful Incomplete pattern match compiler warning that points out the combination that you missed.
And as I argue in my book Code That Fits in Your Head, you should turn warnings into errors. This would also be helpful in a case like this, since you'd be prevented from forgetting an edge case.
Haskell #
You can do the same exercise in Haskell, and the result is strikingly similar:
data ItemStatus = NotAvailable | Available | InUse deriving (Eq, Show) data FieldState = Hidden | ReadOnly | ReadWrite deriving (Eq, Show) decide :: Bool -> Bool -> ItemStatus -> FieldState decide True True NotAvailable = Hidden decide False True NotAvailable = Hidden decide True False NotAvailable = Hidden decide False False NotAvailable = Hidden decide True True Available = ReadWrite decide False True Available = Hidden decide True False Available = ReadOnly decide False False Available = Hidden decide True True InUse = ReadOnly decide False True InUse = Hidden decide True False InUse = ReadOnly decide False False InUse = Hidden
Just like in F#, if you forget a combination, the compiler will tell you:
LibrarySystem.hs:8:1: warning: [-Wincomplete-patterns] Pattern match(es) are non-exhaustive In an equation for `decide': Patterns of type `Bool', `Bool', `ItemStatus' not matched: False False NotAvailable | 8 | decide True True NotAvailable = Hidden | ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^...
To be clear, that combination is not missing from the above code example. This compiler warning was one I subsequently caused by commenting out a line.
It's also possible to turn warnings into errors in Haskell.
Conclusion #
I love languages with algebraic data types because they don't just enable modelling like this, they encourage it. This makes it much easier to write code that handles various special cases that I'd easily overlook in other languages. In languages like F# and Haskell, the compiler will tell you if you forgot to deal with a combination.