WORK-IN-PROGRESS: - this material is still under development
Arrange blocks of code in a data structure to implement an alternative computational model.
Programming languages are designed with a particular computational model in mind. For mainstream languages, this model is an imperative model with code organized in an object-oriented way. This approach is currently favored because it's worked out to be a suitable compromise between power and understandability. However this model isn't always the best one for a particular problem. Indeed often the desire to use a DSL comes with a desire to use a different computational model.
Computational Network allows you to implement alternative computational models within an imperative language. You do this by defining blocks of code and organizing them in some form of data structure. Some mechanism then navigates through this data structure executing these blocks of code according the computational model you're implementing.
One of the first things I like to do when I'm writing about something is to illustrate things with a simple example. It's difficult to do this with an abstract notion such as a Computational Network. Each kind of Computational Network is a relatively involved construct - as a result I've written each one as its own pattern later on in this chapter. All Computational Networks share common characteristics, however, so I felt it would be useful to pull much of this into a common pattern.
Much of the discussion, both here and in the particular examples of Computational Network are focused on the framework aspects of a Computational Network rather than on the DSL. Indeed you don't need a DSL at all to build a Computational Network. However DSLs and Computational Networks are closely connected because a DSL is a particularly useful way of describing an instance of a Computational Network. That's the main reason why I'm spending time describing some example Computational Networks here. Hopefully these examples will give you some starting points for building either your own variations of the example Computational Networks or a new Computational Network that fits your domain.
I mention variations here for a reason. Although there are common characteristics to any particular kind of Computational Network, there's also a lot of room for variation depending on the specific usage. State machines, for example, come in lots of different styles. If you support every feature a state machine can have, you end up with a very complex framework and DSL. Most of the time you only need a small subset of these, so in these cases you should only put the features you need into the framework. If you do need lots of features, it'll often be wise to look for pre-existing libraries that support that Computational Network, but I'm wary of putting too much into a DSL.
There are three parts to defining a Computational Network: defining a data structure in which to place the blocks of code, defining how to navigate over the blocks in order to execute them, and defining the blocks of code themselves.
In an object oriented system, the best way to define the network is through an object graph. Each node on the object graph contains the code block, together with the various objects links needed to fit it into the network. So for a Dependency Network the nodes contain the execution code and the links to the pre-requisites.
There are two broad approaches to navigating the data structure: placing the navigation algorithm in code separate to the structure itself, or embedding the navigation logic in the objects that are part of the graph. My object-oriented instincts tell me to keep the code with the data, and I don't see any reason to move away from these basic instincts here. So in an OO language, I distribute the navigation logic of the Computational Network across all the object nodes.
The final part of this trio is representing the code blocks. There's two broad ways to do this: strategies and closures. With the strategy pattern, each code block is a subclass of a common interface - usually just a run method. Closures are a language specific mechanism to do much the same thing, and are usually the better choice if you have them available.
A DSL is a good choice to go with a Computational Network. The lack of fluency of an API call mechanism makes it difficult to see what is going on within the network. Building up the data structure of the framework is very much a typical DSL action - building up object structures in the framework. On the DSL side the most notable part of working with a Computational Network is defining the code blocks.
There are two approaches here. One is to use DSL clauses to set up
parameters for the code. In this situation I might see something like
score(red(6).blue(2)). The alternative is to actually embed
host code in the DSL script, something like if (ball == Color.RED)
return 6 else if (ball == Color.BLUE) return 2. Extending the
DSL to support parameters gives me the most opportunity to write
something readable at the cost of writing the necessary
Expression Builders. Using embedded
host code (typically using a closure in an internal DSL or Host Code Embedment in an external DSL) provides greater
expressiveness as you can put anything into the code block that the
host language can do. However the host code is more awkward to read -
particularly if you want the DSL scripts to be readable by non-programmers.
The choice between DSL clauses and embedded host code need not be an exclusive one. Often a good route is to use DSL clauses for common cases and reserve host code for uncommon cases. Micheal Feathers told me about how he'd done this, treating the host code as an escape hatch which allowed him to do things that weren't in the DSL. In particular Michael brought out how this was a good evolutionary strategy. He would use host code and look for cases where he was getting similar host code frequently enough to be worth replacing with DSL clauses. This allowed him to use DSL clauses only when they were common enough to pay off the cost of complicating the DSL.
It's particularly valuable to use visualizations for a Computational Network. Most of them respond very well to some from of diagrammatic view. Since the biggest problem of Computational Network is that they are hard to understand, it's usually worth using every tool you can to make things easier.
The glib answer to when to use a Computational Network is that you use it when you want to use an alternative computational model. This, of course, begs the question of when you do want to use an alternative computational model. Here it's a qualitative decision on what best seems to fit your problem. Often an imperative approach works just fine, but there are plenty of occasions where something else seems to be a better fit.
A lot of time this involves considering a common computational model. The other patterns in this section give you a starting point, if one of these seems to fit, then it's worth a try. It's less common to find you want an entirely new computational model, but again it isn't unknown. Often such a realization can grow from the way a framework changes over time. A framework can begin just storing data, but as more behavior worms its way in, you can see a Computational Network beginning to form.
Computational Network comes with a particularly large disadvantage - one that applies to every variation on its use. Computational Networks can be very hard to understand. We are used to our regular computational model, a different model involves thinking differently about how we organize programs. For many people this is a big leap. Here I believe that a DSL (and visualizations) can make a big difference. But even so the tooling for Computational Network doesn't compare to that of your regular programming environment. In particular it's often difficult to debug a Computational Network and you'll usually find you need to put in debug assistance capabilities of some kind.