A combination of function calls as a sequence of statements.
Function Sequence produces a series of calls, unrelated to each other except ordered in a sequence in time; most importantly, there is no data relationship between them. As a result, any relationship between the calls needs to be done through the parsing data, so a heavy use of Function Sequence means you use a lot of Context Variables.
To use Function Sequence in a readable way, you usually want bare function calls. The most obvious way to do this is to use global function calls, if your language allows it. This, however, brings two main disadvantages: static parsing data and the fact that the functions are global.
The problem with global functions is that they are visible everywhere. If your language has some kind of namespacing construct, you can (and should) use that to reduce the scope of the function calls to the Expression Builder. A particular mechanism to handle this in Java is static import. If your language doesn’t support any global function mechanism at all (such as C# and pre-1.5 Java), then you’ll need to use explicit class methods to handle the calls. This often adds noise to the DSL.
Global visibility is an obvious disadvantage to global functions, but often the most annoying problem is that they force you to use static data. Static data is often a problem because you can never be entirely sure who is using it—particularly with multithreading. This problem is particularly pernicious with Function Sequence because you need a lot of Context Variables to make it work.
A good solution for both globally visible functions and static parsing data is Object Scoping. This allows you to host the functions in a class in the natural object-oriented way and gives you an object to put the parsing data into. As a result, I suggest using Object Scoping if you are using Function Sequence in all but the very simplest cases.
On the whole, Function Sequence is the least useful of the function call combinations to use for DSLs. Using Context Variables to keep track of where you are in a parse is always awkward, leading to code that’s hard to understand and easy to get wrong.
Despite this, there are times when you need to use Function Sequence. Often, a DSL involves multiple high-level statements; in this case, a list of statements often makes sense as a Function Sequence as you only need a single result list and Context Variable to keep track of things. So, Function Sequence is a reasonable option at the top level of a language, or at the top level inside a Nested Closure. However, below that top level of statements, you want to form expressions using Nested Function or Method Chaining.
Perhaps the biggest reason to use Function Sequence is that you always have to start your DSL with something, and that something has to be a Function Sequence even if there’s only one call in the sequence. This is because all the other function call techniques require some kind of context. Of course, one can argue about whether a sequence with a single element is really a sequence, but that seems the best way to fit it into the conceptual framework I’m using.
A simple Function Sequence is a list of elements, so the obvious alternative is to use a Literal List.
Here is the recurring computer configuration example as a DSL with Function Sequence:
Although I’ve indented the code to suggest the structure of the configuration, that’s just arbitrary use of whitespace. The script is really just a sequence of function calls with no deeper relationship between them. The deeper relationship is built up entirely using Context Variables.
Function Sequence uses top-level function calls, which I have to resolve in some way. I could use static methods and global state—but I hope that would offend your design taste too much for me to get away with it. So instead I use Object Scoping. This does mean that the script has to be kept in a subclass of the computer builder, but that’s well worth it to avoid using globals.
The builder contains two types of data: the content of processors and disks that it’s building up, and Context Variables to indicate what it’s currently working on.
I’m using Construction Builders to capture the data for the (immutable) Semantic Model objects.
The call to computer()
clears the Context Variables.
The calls to processor()
and disk()
create a value for collecting the data and set the Context Variables to track what the builder is currently working on.
I can then capture the data into the appropriate source.
Specifying the speed is a little more complicated, as the call could refer to either processor or disk speed, depending on the context.
When the builder is done building, it can return the Semantic Model.
To hook all this with the script, I need to wrap the script into a subclass of the computer builder.
3.22.71.106