Another benefit (which doesn’t seem to be in the post) – when you’re building a decision table and realize there’s cases where you don’t know how they should be handled, or (oh no!) you’ve been handling it two different ways, depending on the order different code paths check the variables.
Using match statements in Rust makes encoding decisions tables very clean, which is something I’ve come to like a lot when writing “complex” logic. Encoding the FizzBuzz example in Rust looks like the following code.
let output = match (n % 3, n % 5) {
(0, 0) => "FizzBuzz".to_owned(),
(0, _) => "Fizz".to_owned(),
(_, 0) => "Buzz".to_owned(),
(_, _) => n.to_string(),
};
If we don’t want to enumerate them all (e.g. like the _ wildcards in your fizzbuzz example) then we can calculate booleans for the keys, and lookup whichever is True:
def go(n):
n3 = n % 3 == 0
n5 = n % 5 == 0
return {( n3 and n5): "fizzbuzz",
( n3 and not n5): "fizz",
(not n3 and n5): "buzz",
(not n3 and not n5): str(n)}[True]
I’m not sure if this counts as “simple” or “too clever”, but I’ve found it useful on occasion. The tricky part is that we cannot allow more than one of the booleans to be True at once, since they’ll collide; in this case we need to add and not ... conditions to the fizz and buzz entries to prevent them colliding with the fizzbuzz entry.
I follow Hillel on Twitter and he posts a ton of great content about formal verification, correctness, and programming in general. This particular post is really terrific.
Decision tables are useful on a whiteboard, but manually checking a decision table for completeness can be surprisingly tricky and error prone. They really benefit from being written down in a (programming) language. Then they are quite easy to check for completeness and also pretty easy to minimize (figure out “don’t cares”, columns subsumes by other columns, etc. As an extension, you can assign (multiple) actions to be taken for a certain valuation of the conditions.
The tricky things are supporting a nice way to specify the tables in your language and a sufficiently powerful way of specifying the conditions to evaluate and actions to perform.
I don’t know about Rust.
The matching in F# allows coding up sophisticated match situations. The compiler will tell you if you have left out a case. My favorite example is balancing a Red/Black Tree. This would be many lines of hard to understand imperative code:
type a t =
| Node of color * a * a t * a t
| Leaf
let balance = function
| Black, z, Node (Red, y, Node (Red, x, a, b), c), d
| Black, z, Node (Red, x, a, Node (Red, y, b, c)), d
| Black, x, a, Node (Red, z, Node (Red, y, b, c), d)
| Black, x, a, Node (Red, y, b, Node (Red, z, c, d)) ->
Node (Red, y, Node (Black, x, a, b), Node (Black, z, c, d))
| x ->
Node
Another benefit (which doesn’t seem to be in the post) – when you’re building a decision table and realize there’s cases where you don’t know how they should be handled, or (oh no!) you’ve been handling it two different ways, depending on the order different code paths check the variables.
This is how I have used them in the past. Sometimes I’ve even directly encoded the table as a lookup table, mapping the actions to functions.
Using
matchstatements in Rust makes encoding decisions tables very clean, which is something I’ve come to like a lot when writing “complex” logic. Encoding the FizzBuzz example in Rust looks like the following code.I use this pattern a lot in Haskell with
case.In languages without
match/case/cond/etc. we can use a datastructure to enumerate all the possibilities, e.g. a Python dictionary:If we don’t want to enumerate them all (e.g. like the
_wildcards in your fizzbuzz example) then we can calculate booleans for the keys, and lookup whichever isTrue:I’m not sure if this counts as “simple” or “too clever”, but I’ve found it useful on occasion. The tricky part is that we cannot allow more than one of the booleans to be
Trueat once, since they’ll collide; in this case we need to addand not ...conditions to thefizzandbuzzentries to prevent them colliding with thefizzbuzzentry.Reminded of this paper about Schematic Tables: http://www.subtext-lang.org/OOPSLA07.pdf
Screencast: https://vimeo.com/140738254
From http://www.subtext-lang.org/
John Nagle (“Animats”) suggested they be used for smart contracts since they’re easy for both humans and machines to understand. I liked that idea.
I follow Hillel on Twitter and he posts a ton of great content about formal verification, correctness, and programming in general. This particular post is really terrific.
Decision tables are useful on a whiteboard, but manually checking a decision table for completeness can be surprisingly tricky and error prone. They really benefit from being written down in a (programming) language. Then they are quite easy to check for completeness and also pretty easy to minimize (figure out “don’t cares”, columns subsumes by other columns, etc. As an extension, you can assign (multiple) actions to be taken for a certain valuation of the conditions.
The tricky things are supporting a nice way to specify the tables in your language and a sufficiently powerful way of specifying the conditions to evaluate and actions to perform.
Great post - I admit to being a bit curious to see those rspec macros too, even if it’s not a general approach.
I don’t know about macros; but, I often generate tests from data in rspec. I too am curious what the macros bring to the table.
I found this rspec truth table DSL too. It has a nice syntax…
I really wish I could share them, but it was at an old job and I don’t have the source code :(
I don’t know about Rust. The matching in F# allows coding up sophisticated match situations. The compiler will tell you if you have left out a case. My favorite example is balancing a Red/Black Tree. This would be many lines of hard to understand imperative code:
type a t =
let balance = function