Continuation Style Without the Fugly

The previous post discussed advantages of using a continuation style to send our code to the data instead of making our code wait for the data to come back to it.

This pseudocode example has three I/O operations, each encasing the operations which should follow in a callback function parameter.

let filename = // calculate filename
new File(file).readAllLines().andThen( { data ->
// filter the data
// summarize the data
// reformat the data
new File("output").writeAllLines(newData).andThen( { status ->
println("done!")
sendEmail("done! status = " + status).andThen( {
println("email sent")
})
})
})

There’s an obvious negative here: it’s ugly. All this stuff happens in a clear sequence, the same sequence as in the first pseudocode, but it’s hard to see that with all those indentations.

It would be nice if we could describe this without all those curly braces and indentation.
F# async workflows are an example of that goal achieved. The following is pseudocode stuck in the middle of an F# async workflow block.

async {
let filename = // calculate filename
let! data = new File(file).readAllLines();
// filter the data
// summarize the data
// reformat the output
let! status = new File("output").writeAllLines(newData);
println("done!");
do! sendEmail("done! status = " + status);
println("email sent");
} |> Async.Start

This is precisely what we’d like the code to look like, with a few exceptions. The “async” at the beginning and the piping of the block to Async.Start are the F# overhead to trigger the magic asynchronicity.
The let! and do! keywords are where the magic happens: they trigger F# to wrap the rest of the block into a super-sneaky callback that gets passed to an asynchronous operation which evaluates the expression in the let! or do! in another thread. When the file read is complete, the rest of the code proceeds, in whatever thread it’s in.

The second example executes exactly like the first one. But it reads sooo much more smoothly!

The tricky bit is that let! and do! sneakily break up the block of code. Anything after the let! may happen in a different thread, and then the next let! might switch execution to yet another thread. As a programmer we don’t have to worry about that, but it’s mind-bending when the code looks so sequential.

I hope we’ll see more languages and frameworks that can operate like this: pass the code to the data, but do so in a manner that keeps our code readable and organized.

Bring the data to the code, or the code to the data?

Object-oriented code was conceived as message-passing between objects. Service-oriented architecture emphasizes delegation to another system. The entire web is a whole bunch of requests flying around. There is one clear way to be efficient about this: stop waiting for results.

When we’re writing imperative code, we want to write the operations in the order they should happen. This is straightforward and makes sense to our brains. Pseudocode:

let filename = // calculate filename
let data = new File(file).readAllLines();
// filter the data
// summarize the data
// reformat the output
let status = new File("output").writeAllLines(newData);
println("done!");
sendEmail("done! status = " + status);
println("email sent");

This describes the order in which operations need to happen. The problem is, it is not efficient. We’re holding up a thread waiting for I/O. Take the part where we read lines from the file, for instance — we can’t proceed until we get that result back, right? the rest of our code needs that data.

There is an alternative.

Instead of bringing the data to our code, we can ship our code to the data.
With functions-as-values, we can send our code along with the request for the data. This frees up our thread to continue processing, and then our code can execute when the data is ready. When passed as a parameter, the code to execute after completion is known as a callback or a continuation.

Instead of waiting for the data to come back from the file read, we can pass the code that needs to operate on the data. That way whatever thread winds up with the data can execute the code: code and data are brought together.

The pseudocode example has three asynchronous operations. In each case we can change the rest of the code in the block into a callback.

let filename = // calculate filename
new File(file).readAllLines().andThen( { data ->
// filter the data
// summarize the data
// reformat the data
new File("output").writeAllLines(newData).andThen( { status ->
println("done!")
sendEmail("done! status = " + status).andThen( {
println("email sent")
})
})
})

When this executes, the filename is calculated, the read is triggered and then our program goes about its business doing whatever’s next. Everything needed to process the data is bottled up in that function we passed, that continuation. We’re passing the code to where the data is, instead of freezing the code in place until the data is available.

The idea of putting functions into values and passing them to the data, instead of bringing data back to the code, facilitates the message-passing that OO was based on. It facilitates a faster service-oriented architecture. It can make a faster web. JavaScript is all over this technique; AJAX and Node.js use this principle.

Continuation style is a lovely combination of imperative style — everything happens in the order specified — with the functional concept of code-as-data. It frees the browser or the runtime to optimize and keep open threads busy.

If your reaction is, “yeah, but it’s fugly!” then look for my next post.