Compile-Time Checked Truth Tables
blog.ploeh.dk
blog.ploeh.dk
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
...to something like this: let decide requiresApproval canUserApprove itemStatus =
match requiresApproval, canUserApprove, itemStatus with
| _, _, NotAvailable -> Hidden
| false, _, _ -> Hidden
| true, true, Available -> ReadWrite
| true, _, _ -> ReadOnly
I would think that being able to see and effect this reduction is part of the benefit of writing out the whole table. It might not always be the right call to actually reduce it, but in this case (based on the parameter names) each of the branches seems to makes sense from a domain perspective, so why not do it?Because wildcard pattern matches are error prone.
When you add a new constructor, the compiler will remain satisfied that the pattern match is exhaustive and won't prompt you to verify the behaviour.
EDIT: Oh, actually, nevermind. After reading more closely, I do agree that it might be reduced a bit too much!
That's my problem with var in Java
True.
Unfortunately, they can also be asymptotically shorter than writing the whole thing out: for example, try writing a lexicographic comparison function for an AST type of your choosing and making it linear in the number of constructors.
If the language is lazy, it can be more severe than that: in Haskell,
andL False _ = False
andL _ rhs = rhs
is the short-circuiting AND, while andS False False = False
andS False True = False
andS True False = False
andS True True = True
is the naïve one.Later, I borrowed a trick learned from someone else and used a hashtable to store the relevant data (written to only once when the program starts) instead. At this point, the performance cost was so small that I couldn't tell any difference compared to before I introduced pattern matching.
For what it's worth, there are about 18000 characters specified by Unicode for special handling with grapheme breaks[0] so I don't know how noticeable the difference would be with the pattern matching tables you're likely to encounter in real life.
[0] https://www.unicode.org/Public/UCD/latest/ucd/auxiliary/Grap...
For Unicode tables, the usual approach is of course a radix trie, even if the traditional two-level trie from the Unicode 2.0 days is starting to look a bit bloated now and a three-level tree is quite a bit slower. A hash might be a good approach for some things. One potential downside is that it completely destroys memory locality, while normal Unicode data will usually use a fairly small subset of the characters; I'm not sure how important this would turn out to be. People (e.g. libutf and IIRC duktape) have also used a sorted table of alternating start- and endpoints of ranges.
[The 18k figure sounds scarier than it is: 11k of those are precomposed Korean syllables encoded in a well-known systematic way, so for 0xAC00 <= codepoint < 0xAC00 + 19 * 21 * 28 the table just says (if (codepoint - 0xAC00) mod 28 = 0 then LV else LVT).]
match (requires_approval, can_user_approve, item_status) {
(true, true, Available) => ReadWrite,
(true, false, Available) => ReadOnly,
(true, _, Inuse) => ReadOnly,
(true, _, NotAvailable) => Hidden,
(false, _, NotAvailable | Available | InUse) => Hidden,
} enum ItemStatus { notAvailable, available, inUse }
enum FieldState { hidden, readOnly, readWrite }
FieldState decide(bool requiresApproval, bool canUserApprove, ItemStatus itemStatus) =>
switch ((requiresApproval, canUserApprove, itemStatus)) {
(true, true, ItemStatus.notAvailable) => FieldState.hidden,
(false, true, ItemStatus.notAvailable) => FieldState.hidden,
(true, false, ItemStatus.notAvailable) => FieldState.hidden,
(false, false, ItemStatus.notAvailable) => FieldState.hidden,
(true, true, ItemStatus.available ) => FieldState.readWrite,
(false, true, ItemStatus.available ) => FieldState.hidden,
(true, false, ItemStatus.available ) => FieldState.readOnly,
(false, false, ItemStatus.available ) => FieldState.hidden,
(true, true, ItemStatus.inUse ) => FieldState.readOnly,
(false, true, ItemStatus.inUse ) => FieldState.hidden,
(true, false, ItemStatus.inUse ) => FieldState.readOnly,
(false, false, ItemStatus.inUse ) => FieldState.hidden,
};
As with F# and Haskell, the compiler will tell you if you didn't cover one of the combinations. In the case of Dart, we go ahead and make it a full compile-time error, not just a warning.The cool thing about pattern matching over tuples with compile-time exhaustiveness checking is that you can start simplifying and collapsing rows and the compiler will let you know if you made a mistake.
I believe you can simplify the example here to:
FieldState decide(bool requiresApproval, bool canUserApprove, ItemStatus itemStatus) =>
switch ((requiresApproval, canUserApprove, itemStatus)) {
(_, _, ItemStatus.notAvailable) => FieldState.hidden,
(false, _, ItemStatus.available ) => FieldState.hidden,
(false, _, ItemStatus.inUse ) => FieldState.hidden,
(true, false, ItemStatus.available ) => FieldState.readOnly,
(true, _, ItemStatus.inUse ) => FieldState.readOnly,
(true, true, ItemStatus.available ) => FieldState.readWrite,
};
Doing that simplification is a useful exercise to then determine how to express the rules you're modeling in a succinct way. var thing = (enumA, enumB) switch
{
(EnumA.A,EnumB.A) => DoStuff(),
_ => throw new Exception(),
}
suffice to say, c# is pretty ok, coming from ml family. match (i % 3 == 0, i % 5 == 0) {
(false, false) => { },
(true, true) => { FizzBuzz },
(true, false) => { Fizz },
(false, true) => { Buzz }
}
I'm moving from C# to Rust at the moment and really enjoying it, especially now that I (almost) understand the borrow checker[0] https://stackoverflow.com/questions/43896431/c-sharp-equival...
foreach (var i in Enumerable.Range(1,100))
{
Console.WriteLine(
(i%3==0,i%5==0) switch
{
(true, true) => "FizzBuzz",
(true, _) => "Fizz",
(_, true) => "Buzz",
(_, _) => $"{i}"
}
);
}
This is so much nicer than what was possible back when the linked question was asked. (i%3,i%5) switch {
(0, 0) => "FizzBuzz",
(0, _) => "Fizz",
(_, 0) => "Buzz",
(_, _) => i
}Subtext [2] included a feature called "schematic tables" that is related to decision tables (demo [3], 8yr old video).
When I saw Subtext's schematic tables I started to think of minimizing expressions, finding the complement, etc, to aid in writing correct boolean expressions. I think an algorithm like Quine–McCluskey [4] or espresso [5] could have applications to programming for refactoring boolean logic (normally these are used in electronics design).
I don't know of any programming environment that could tell you if a boolean expression is a tautology or a contradiction... (I imagine a linter with SAT solver [6] powers could do this). In table form this would be obvious: a result column that is always true or always false. But then again you don't want to write a truth table for EVERY boolean expression on a program. Would be nice to be able to go back and forth from table to expression, minimize the expression, check satisfiability, etc.
--
1: https://markusvoelter.medium.com/the-evolution-of-decision-t...
2: https://www.subtext-lang.org/
3: https://vimeo.com/140738254
4: https://en.wikipedia.org/wiki/Quine%E2%80%93McCluskey_algori...
5: https://en.wikipedia.org/wiki/Espresso_heuristic_logic_minim...
6: https://en.wikipedia.org/wiki/Boolean_satisfiability_problem
refs: [1]: http://moscova.inria.fr/~maranget/papers/ml05e-maranget.pdf https://julesjacobs.com/notes/patternmatching/patternmatchin...