Chapter 38. Nested Closure

Express statement subelements of a function call by putting them into a closure in an argument.

image

38.1 How It Works

The basic idea of a Nested Closure is similar to that of Nested Function, but the child expressions of the function call are wrapped in a closure. To show the difference, here’s a call to create a new processor using Nested Function in Ruby:

image

Now with a Nested Closure:

image

Instead of passing two Nested Function arguments, I pass a single Nested Closure argument which contains the two Nested Functions. (I’m using Ruby here, as it provides closures in a syntax which is suitable for this discussion.)

Placing the subelements in a Nested Closure has an immediate consequence for my implementation—I have to put in code to evaluate the closure. With a Nested Function, I don’t need to do this since the language automatically evaluates the cores and i386 functions before calling the processor function. With a closure argument, the processor function is called first and the closure is only evaluated when I explicitly program it to. So, usually I’ll evaluate the closure within the body of the processor function. The processor function can also carry out other tasks before and after the closure evaluation, such as setting up Context Variables.

In the example above, the contents of the closure is a Function Sequence. One of the problems of a Function Sequence is that the multiple functions communicate using hidden Context Variables. While you still have to do this inside a Nested Closure, the processor function can create the Context Variable before evaluating the closure and tear it down afterwards. This can greatly reduce the problem of Context Variables appearing all over the place.

Another choice for the subelements is to use Method Chaining. Here, there is the additional benefit that the parent function can set up the head of the chain and pass it into the closure as an argument.

image

It’s also quite common to pass in a Context Variable as an argument.

image

In this case, we have a Function Sequence but with the Context Variable explicitly present. This often makes it easier to follow, without adding too much clutter.

Bare functions written inside a Nested Closure are evaluated in the scope where they are defined—so, again, it’s usually wise to use Object Scoping. Passing in an explicit Context Variable or using Method Chaining allows you to avoid this, as well as to organize the builder code into different builders.

Some languages allow you to manipulate the context that a closure is executed in. This can allow you to use bare functions and still use multiple builders. The example with Ruby’s instance_eval (“Using Instance Evaluation (Ruby),” p. 412) shows how this can work.

In the examples I’ve shown above, I’ve put all the subelements of the parent function into a single closure. It’s also possible to use multiple closures. The advantage of this is that it allows you to evaluate each subclosure independently. A good example of where this is handy is where you have a conditional expression, such as this example in Smalltalk:

image

38.2 When to Use It

Nested Closure is a useful technique because it combines the explicitly hierarchic structure of Nested Function with the ability to control when the arguments are evaluated. Control of evaluation provides you with a lot of flexibility, helping you to avoid many of the limitations of Nested Function.

The biggest limitation of Nested Closure is the way the host language supports closures. Many languages don’t provide closures at all. Those that do often provide the syntax in a way that doesn’t jive terribly well with DSLs, such as with an awkward keyword.

It’s usually worth thinking of Nested Closure as a enhancement to Nested Function, Function Sequence, and Method Chaining. The explicit control of evaluation gives you different advantages with each technique. All of these, however, boil down to the fact that you can do specific setup and tear-down operations on either side of the closure invocations. With Function Sequence, this means you can prepare Context Variables right before they are used by the closure. With Method Chaining, you can set up the head of the chain before invoking the closure.

38.3 Wrapping a Function Sequence in a Nested Closure (Ruby)

For a first example, I’ll start with what I might consider as the most straightforward case: using a Nested Closure in conjunction with a Function Sequence. Here is the DSL script:

image

To begin the discussion, let’s compare this with a version using Function Sequence alone, which would look like this:

image

From the script’s point of view, the only change with Nested Closure is to add the do...end closure delimiters. By adding these, I introduce an explicit hierarchic structure to what otherwise is a linear sequence with a formatting convention. The extra syntax doesn’t strike me as troubling because it’s marking the structure from the reader’s point of view and in a way that makes sense to the reader.

Now, let’s move on to the implementation. As usual, I’m using Object Scoping so I can have bare functions resolve against an Expression Builder. (A note for rubyists: I’m using subtyping here for pedagogical reasons, but with Ruby I’d usually use instance_eval.) We can see the basic structure of using Nested Closure by looking at the clause for computer.

image

I pass in the closure as an argument (Ruby refers to closures as “blocks”), set up some context, and then call the closure. The processor function can then use this context and repeat the process for its children.

image

I do the same for the disks. The only difference is that this time, I use the more idiomatic yield keyword to call the implicitly passed-in block. (This is a mechanism Ruby uses to simplify working with a single block argument.)

image

38.4 Simple C# Example (C#)

For contrast, here is pretty much the same example using C#:

image

As you can see, the structure is exactly the same as in the Ruby example; the big difference is that there’s a lot more punctuation in the script.

The builder also looks remarkably similar.

image

The computer function follows the same pattern that we see in the Ruby case: pass the closure argument, do any setup, invoke the closure, then do any tear-down. The most notable difference with C# is that we have to define the type of the closure we pass in with a delegate clause. In this case, the closure has no arguments and no return type, but with a more complicated case we might need several different types.

The rest of the code is similarly similar to the Ruby case, so I’ll save the ink. To my eyes, Nested Closure works much less well in C# than it did in Ruby. Ruby’s do...end closure delimiters flow more naturally to me than C#’s () => {...}, particularly when you also add the mandatory parentheses into the mix. (You can also use {...} as closure delimiters in Ruby.) The more used you are to C# notation, the less that will bother you. Furthermore, this example doesn’t pass arguments into the closure—which adds more punctuation to the Ruby case but actually makes the C# easier to read since the empty parentheses now have something to surround.

38.5 Using Method Chaining (Ruby)

Nested Closure can work in a couple of different styles. Here’s an example using Method Chaining:

image

The difference here is that each call passes in an object to the closure that’s used at the head of a chain. This use of closure arguments may add noise to the DSL script (as does the need to now wrap method arguments in parentheses), but one benefit is that you no longer need Object Scoping and thus can easily use the code in a fragmentary style.

Invoking the build method creates an instance of the builder and passes it into the closure as an argument.

image

Another useful part of this approach is that it makes it easy to factor the various builder methods into a group of small, cohesive Expression Builders. The processor clause introduces a new builder (using the more compact yield keyword).

image

Disks are also handled with a disk builder.

image

Apart from allowing better factoring of the builder methods, it also allows me to use an unqualified speed method for both the processor and the disk without ambiguity.

38.6 Function Sequence with Explicit Closure Arguments (Ruby)

In the previous example, we saw there are several advantages to breaking down the language layer into multiple Expression Builders. With this approach, each builder is smaller and more cohesive; we also allow clauses in different parts of the language to use the same name (as in processor and disk speed). The explicit closure arguments also allow us to easily use the DSL in a fragmentary context.

While Method Chaining gives us these advantages, the resulting DSL script can look rather awkward. The interplay between Nested Closure and Method Chaining doesn’t necessarily fit will. Certainly, most of the Ruby DSLs I’ve looked at do not use this style.

Instead, they use Function Sequence within each closure but pass an explicit closure argument to allow multiple builders. In this style, our computer configuration script looks like this:

image

The big difference in the DSL script is that you have separate statements for each clause in the DSL. On every statement, you have to state the passed-in object as the receiver of the method call. Although this adds more text to the statement, it results in a more regular style of code that rubyists find easier to work with.

The implementation is very similar to the Method Chaining case. Again, I have a computer builder at the top level with a class method that creates an instance and passes it to the supplied closure.

image

The processor clause introduces a new builder.

image

I’ll leave you the barely existing challenge of figuring out what the disks look like.

38.7 Using Instance Evaluation (Ruby)

Passing in explicit closure arguments yield many advantages, but at the cost of constantly mentioning the name of the argument. Ruby gives us a particularly nifty technique to help deal with this: instance evaluation (using the instance_eval method).

When you call a Ruby block, the block is evaluated in the context of where it’s defined. In particular, any bare functions (or fields) are resolved to the object in which it’s defined. Using instance_eval, you can change this by telling some other object to execute the block within its context, which means any bare methods will now resolve to the new object. The following code demonstrates the difference:

image

In effect, using instance_eval changes what self refers to inside the passed-in block.

We can use this facility to be able to use multiple builders with bare method calls in our DSL script.

image

The builder takes the block as it did before, but uses instance_eval rather than call:

image

Handling the processor again uses instance_eval:

image

As does the disk:

image

The way I’ve shown the DSL script use instance_eval is typical for a fragmentary context where I’m putting a little bit of DSL into a regular Ruby program. In a self-contained context, I can have the DSL script in its own file, in which case by using instance_eval I get rid of any top and tail noise of setting up Object Scoping. The whole script file looks like this:

image

The builder can then process the whole file by instance_evaling it.

image

Using instance_eval seems such a good trick that you may wonder if you should ever pass explicit closure arguments. As it turns out, there is a very real choice, one that was crystallized for me by Jim Weirich’s experience with his builder library. The builder library is a very nice library to create XML documents using Nested Closures and Dynamic Reception. In the first version of the library, Jim used instance_eval, but later switched to explicit parameters. The reason is that programmers are used to the call behavior with closures; redefining self causes a lot of confusion and makes it very difficult to refer to elements in the static context that you need.

For me, the choice lies in whether you are using the DSL script in a self-contained or fragmentary style. In a fragmentary context, you need to follow the usual conventions with closures, so redefining self though instance_eval is not a good choice. With self-contained DSL scripts, your code style is different from regular Ruby code; the redefinition then doesn’t cause confusion and is worth it to remove the noisy references.

..................Content has been hidden....................

You can't read the all page of ebook, please click here login for view all page.
Reset
3.129.216.7