Thursday, March 22, 2012

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.

3 comments:

  1. Technically, let! and do! aren't keywords--they're functions defined in the F# core library to work their async magic. (How, I couldn't tell you. Something to do with monads or monoids or hominids or something.) Which means, of course, that an F# developer who doesn't like the semantics of let! and do! can define his/her own, if he/she chooses.

    Which just rocks, if you ask me. :-)

    ReplyDelete
  2. Using Akka Futures in Scala you could do something like this:

    val filename = // calculate filename

    for {
    data <- Future { new File(file).readAllLines() }
    // filter the data
    // summarize the data
    // reformat the output
    status <- Future { new File("output").writeAllLines() }
    _ <- Future { sendEmail("done! status = " + status) }
    } {
    println("email sent")
    }

    Just like F# computation expressions, the secret sauce is monads.

    ReplyDelete
    Replies
    1. Awesome! More languages and libraries seem to be moving toward this write-imperative, run-async idea.

      There's even a JS implementation of F# computation expressions out there.

      This seems to be spreading. That's a good sign.

      Delete