This article illustrates a use case where a simple-to-implement lookup table can be used as a better alternative to hard coding decision logic. The coding trick used here should be applicable to any programming language with multi-dimensional associative arrays.
When would you want to do this?
A problem I've faced more than a few times is having a set of variables, perhaps aggregating data from various sources, and needing to determine what overall state, from a limited set of valid states, is being represented by those values. My first cut at the problem would probably be to draw a decision tree, considering each variable in turn, and then try to implement that tree's logic using nested if-then-else and/or case (switch) statements. Depending upon the scope and nature of the problem, this could result in a lot of logic that may be hard to follow, debug, or extend.
One possible alternative is to create a lookup table that enumerates all the expected values for each variable, and to assign the desired state represented by each row of that table. Creating the table may be a bit tedious, but it can be more easily understood by non-coders, and the logic for doing the lookup can be near trivial. And perhaps the biggest win is the simplicity in adding, removing, and modifying states by updating the table, rather than hacking on the logic.
Let's consider a simple example; ordering a car where there are five different trim lines and a set of options. Not all options are available for all trim lines. Here's what that might look like:
Trim | Interior | Wheels | Transmission | Cruise |
---|---|---|---|---|
Base | cloth | steel | alloy | manual | n/a |
Premium | cloth | leather | alloy | manual | auto | n/a |
Sport | sport-cloth | 18" black alloy | 6 speed manual | opt |
Limited | cloth | leather | alloy | manual | auto | std |
Touring | leather | 18" alloy | auto | std |
We've been tasked to write a function that determines if a set of dealer selected options are allowed for a given trim line, and to identify a valid combination of options and trim as a particular 'bundle' identifier. Our first step will be to expand the table so that all possible valid combinations are enumerated, and we'll simplify the identifiers a bit.
Trim | Interior | Wheels | Transmission | Cruise | Bundle |
---|---|---|---|---|---|
Base | cloth | steel | manual | 0 | B1 |
Base | cloth | alloy | manual | 0 | B2 |
Premium | cloth | alloy | manual | 0 | P1 |
Premium | leather | alloy | manual | 0 | P2 |
Premium | cloth | alloy | auto | 0 | P3 |
Premium | leather | alloy | auto | 0 | P4 |
Sport | sport | sport | sport | 0 | S1 |
Sport | sport | sport | sport | 1 | S2 |
Limited | cloth | alloy | manual | 1 | L1 |
Limited | leather | alloy | manual | 1 | L2 |
Limited | cloth | alloy | auto | 1 | L3 |
Limited | leather | alloy | auto | 1 | L4 |
Touring | leather | touring | auto | 1 | T1 |
When we are convinced that we have enumerated all of the valid possible combinations, we can turn this table into a multi-dimensional hash. The resulting Perl code, shown below, bears an uncanny resemblance to our table.
%bundle = ();
# Trim Interior Wheels Trannie Cruise Bundle
# --------- --------- --------- -------- ------ ------
$bundle {base} {cloth} {steel} {manual} {0} = 'B1';
$bundle {base} {cloth} {alloy} {manual} {0} = 'B2';
$bundle {premium} {cloth} {alloy} {manual} {0} = 'P1';
$bundle {premium} {leather} {alloy} {manual} {0} = 'P2';
$bundle {premium} {cloth} {alloy} {auto} {0} = 'P3';
$bundle {premium} {leather} {alloy} {auto} {0} = 'P4';
$bundle {sport} {sport} {sport} {sport} {0} = 'S1';
$bundle {sport} {sport} {sport} {sport} {1} = 'S2';
$bundle {limited} {cloth} {alloy} {manual} {1} = 'L1';
$bundle {limited} {leather} {alloy} {manual} {1} = 'L2';
$bundle {limited} {cloth} {alloy} {auto} {1} = 'L3';
$bundle {limited} {leather} {alloy} {auto} {1} = 'L4';
$bundle {touring} {leather} {touring} {auto} {1} = 'T1';
Now to determine if we have been given a valid combination of trim and options, and to return the bundle code, we just do:
$trim = 'touring';
$seats = 'leather';
$wheels = 'touring';
$trans = 'auto';
$cruise = 1;
$bCode = $bundle{$trim}{$seats}{$wheels}{$trans}{$cruise};
If $bCode
is defined, then we know the combination is valid and we have our bundle code (T1
).
Notice how adding a new trim line or a new option is just a matter of modifying the table with little or no change to the lookup statement. In contrast, a block of if-then-elsif statements might not be so easy to hack and debug.
An interactive command-line demo script, which also includes error handling and validation, can be found here.
Top comments (5)
Watch out for autovivication.
You bet! I glossed over that in the code example in this article because I wanted the code to be as clear as possible for non-Perl folks. I'm hoping that people who are interested in the Perl aspect will take the time to look at the demo. It uses exists() instead of defined() when testing against the table for just that reason. In the demo I also wanted to avoid dependancies. But at some point I would like to circle back and explore if the lookup table can be made read-only after it is built, which would be better still. I just need to take the time to explore modules like ReadonlyX.
ReadonlyX is a great option for this. Note that exists and defined both still autovivify any intermediate structures - e.g. if you are looking at
$foo->{bar}{baz}
then$foo
will be autovivified to{bar => {}}
regardless of what the end check is (the final element will not be vivified unless assigned to). Two options for avoiding this are autovivification and my Data::DeepAccess.Very interesing, What would it be the equivalent in Javascript?
It looks like you might use a Javascript object rather than an associative array to build the table with a similar easy to read syntax. Check out this stack overflow question. Beyond that I can't say since I don't have Javascript in my tool belt yet. (On my todo list.)