The Evolution of Decision Tables
A little study in language design
Many domain experts like tables as a way to representing decisions and calculations. They perceive it to be more readable than “code”. The popularity of Excel is certainly at least partly due to this effect. The OMG’s DMN also uses tables extensively for representing decisions (so-called decision tables). There are many forms of decision tables, and we have been using them in our DSLs for a long time.
Their expressiveness has evolved quite a bit, though. In this article I recount the evolution of decision tables based on inputs from our users. It is a nice example of language design, and a good illustration of why it is useful to be able to evolve the language when you are dealing with business people.
Basic two-dimensional Decision Tables
Years ago, we had implemented
the basic two-dimensional decision table in mbeddr, our extensible version of C optimized for embedded
This table is an expression, so you can use it wherever C expects an expression. And it has a set of Boolean conditions as row and column headers. The conditions themselves are kinda independent, but usually, every dimension addressed one variable (spd vs. alt). So essentially, this table represents a set of nested if statement over two criteria.
We have since implemented the
exact same structure for KernelF,
the functional base language we use as the basis of business DSLs. Also, in a customer
project in the healthcare domain, we have collaboratively built a version
of this table that explicitly concerns two numeric variables, which made the
syntax much less cluttered:
Generalized N-Criteria Table
This form of decision tables
gives up on arranging the two criteria along the two table dimensions, and
instead lists them as separate columns. Here is a simple example that
calculates some kind of base fare based on the state(an enumeration
type) the customer lives in and whether she is a member of some kind of club (a
The tables are evaluated top-down (so more specific criteria have to be mentioned further up), and an empty field means “don’t care”. So the table calculates a fare of 1.00 if you live in BW, the fare in BY depends on whether you are in the club, and everybody else pays 1.20.
Multiple Return Values
Turns out that often there is
more than one return value that depends on the criteria. So we allow multiple
result columns which are represented as a tuple value in terms of KernelF’s
type system, as the explicit return type of the function below illustrates:
The columns in a decision
table are joined by logical “and”. However, sometimes you also want to express
an “or”. For example, the 15%-discounting might apply to Bavaria (BY) and Hesse (HE). We’ve
implemented the comma operator to express this (note that you cannot directly use
the existing || operator because the two enum literals are
not Booleans; we could have overloaded the typing rules though):
In all examples so far you
don’t specify a comparison operator; the table implicitly compares with
“equals”. However, as we have seen in the medical example above, we often want
to compare ranges for numerical values. For example, the base fare could change
for children in Baden-Württemberg (BW):
Note that these things in the age column are not really complete expressions, they take the column value as an implicit argument. However, it is important to be able to write it this way and not mention the value (age) every time because that would increase verbosity and error-proneness.
Embedding the Tables into Context
In the examples we have seen so far, the tables are used as expressions. Since KernelF is a functional language, expressions are everywhere, and making tables expressions makes allows their use almost everywhere.
Top Level Tables
Very often, though, the
tables are the only expression in a function. This works, of
course, but if you look closely, there’s a bit of duplication: the column
headers refer back to the argument. This is why we have provided a top-level
version of decision tables where this duplication is avoided because the query
columns act as parameter declarations (so they have to specify a type now):
It is surprising how much difference this makes to many domain experts — they don’t have to explicitly understand the concept of a function (even though, semantically, the table of course is one).
We use the decision tables in
a DSL where we have function-like calculations, but the result data structure
is populated by assigning to its members. Here is how the decision tables can
be used in this context:
We first assign the (tuple)
value returned by the decision table to a local value f (which is
inferred to be of type [Currency, Percentage, a tuple type) and we then
use the tuple’s native position-based indexing to assign to the result fields (base and discount are
members of the Fare record). Again: this
works, but it is verbose because of the intermediate value f and the
subsequent position-based assignment. To solve this issue we support assignment
to tuple values if all of the elements of the tuple can be assigned to (i.e.,
One problem remains, though:
the names and types of the result columns are kinda redundant, because both can
be automatically derived from the values we assign to. So why don’t we just put
the assignment targets into the result columns?
Now, this is a nice compact notation. Our customers really liked that one. And justifiably so!
It’s still all Expressions!
Despite the specific notation, we are still in the context of a a “full” functional language. For example, you can have more complex expressions in the conditions …
… and you can use local
values to factor out more complex calculations (there are literally refactoring
operations to extract local values):
We feel that this really is combining the best of both worlds: expressive programming and “declarative”, table-based decision making.
Collaboration with our Customers
So why and how did this evolution happen? Basically, it is because of our customers. They get the point about language engineering in the sense that they drive us to create less verbose and more end-user friendly notations. It is our job then, as language engineers, to find solutions that satisfy the users, but also retain the integrity of the language (both KernelF and the DSL we are developing for them) in terms of orthogonality, composability and modular implementation. For example, it is perfectly ok to build special support into decision tables so they can “assign to” lvalues, but we don’t want the lvalues to know something about the decision tables. Sometimes this means that we cannot (don’t want to) implement our customer’s wishes to 100%.
It is a really good collaboration if we are willing to take the customers’ needs and wishes seriously, but they also understand our concerns about the language design and implementation (and the resulting slightly lower than 100% wish-fulfilment rate :-)).