Joose blog

Blog about Joose – advanced class system for JavaScript

JooseX.CPS Tutorial – Part I

with 2 comments

So, after the recent crack of our server, the post about the JooseX.CPS was lost. It was poorly written anyway, so may be its even better that the server was hacked – because I’m writing a new post. JooseX.CPS is quite important extension, and its being extensively used in many other Joose-based projects, like KiokuDB, Syncler and Symbie. The new post is written in form of tutorial and contains a runnable code. At the end of this post you should grok the continuation passing style :)

Setup

To follow this tutorial you will need:

The latter can be installed with `npm install task-joose-nodejs`.

Part I – Meet The Continuation

The tutorial will be split into 2 parts. In this part we’ll examine the low-level JooseX.CPS.Continuation class and its features. The second part will describe a JooseX.CPS trait itself, which provides a CPS sugar layer on top of JooseX.CPS.Continuation for you Joose classes.

Just try it, aka “Hello world”

Before we’ll dig in, lets see how the “hello world” will looks like in CPS world:

require('task-joose-nodejs')

TRY(function () {

    console.log('Hello')

    this.CONT.CONTINUE()

}).DELAY(1000).THEN(function () {

    console.log('world!')

}).now()

Try to launch this in your Node. You should see the following with 1s delay between the words:

nickolay@desktop:~/Documents/JooseBlog/CPS-Tutorial$ node hello_world.js
Hello
world!

Compare with

console.log('Hello')
console.log('world!')

You see the idea? With some additional boilerplate between them, the synchronous the statements work asynchronously, thats it. And as we’ll move forward in this tutorial, the amount of boilerplate will reduce, really.

Continuation anatomy

In the first approximation, continuation is an array of tasks, also containing the “catch” and “finally” functions:

CONT = {

    tasks           : [
        {
            func    : function doThis() { ... },
            scope   : {},
            args    : [ ... ]
        },
        {
            func    : function doThat() { ... },
            ...
        },
        ...
    ],

    catchFunc       : function () { ... },
    finallyFunc     : function () { ... },
}

The “task” is simply a function (mandatory), plus a scope for its execution and an array of arguments (both optional). Such continuation roughly corresponds to the following “synchronous” code:

try {

    doThis()
    doThat()

} catch (e) {

    cathcFunc(e)

} finally {

    finallyFunc()
}

Once more – continuation is a single “try/catch” block, containing an array of tasks. You can add tasks to the continuation with the `TRY(func, scope, args)` method. Adding tasks will not launch them immediately – you need to “activate” the continuation manually with its method `now`.

After activation, continuation will launch the first task. It will embed a new, fresh continuation instance into the scope of task, as the `this.CONT`. To pass the control flow from the 1st task to the next one, you need to call the `CONTINUE` method of the embedded continuation. Any parameters, passed to `CONTINUE` will be available as the arguments of the next task. Lets re-write the “Hello world” example, “by hands”:

require('task-joose-nodejs')

var CONT    = new JooseX.CPS.Continuation()

var scope   = { toString : function () { return 'My scope' } }

CONT.TRY(function () {
    console.log("Current scope 1: %s", this)

    console.log('Hello')

    // a new, fresh instance of JooseX.CPS.Continuation has been embedded:
    // this.CONT != CONT
    this.CONT.CONTINUE('world', '!')

}, scope)

CONT.TRY(function (str1, str2) {
    console.log("Current scope 2: %s", this)

    console.log(str1, str2)
})

CONT.now()

Some sugar: there is `THEN` synonym for the `TRY` method, which sounds more naturally for the 2nd and further tasks. Note, that if you have not provided a scope for the task, it will be taken from the previous task.

The call to `CONTINUE` doesn’t have to be done synchronously, you can delay it. To do that, capture it to the closure with the `getCONTINUE` method. Lets add the 1s pause between the outputs, “manually” (w/o the `DELAY()`):

require('task-joose-nodejs')

var CONT    = new JooseX.CPS.Continuation()

var scope   = { toString : function () { return 'My scope' } }

CONT.TRY(function () {
    console.log("Current scope 1: %s", this)

    console.log('Hello')

    // this.CONT != CONT - its a new, fresh instance of JooseX.CPS.Continuation
    // passing the parameters further
    this.CONT.CONTINUE('world', '!')

}, scope)

CONT.THEN(function (str1, str2) {

    var CONTINUE = this.CONT.getCONTINUE()

    setTimeout(function () {

        CONTINUE(str1, str2)

    }, 1000)
})

CONT.THEN(function (str1, str2) {
    console.log("Current scope 2: %s", this)

    console.log(str1, str2)
})

CONT.now()

Hey don’t scan, read the sources :)

Instead of manual instantiation of JooseX.CPS you can use global `TRY` helper which just creates a new instance and delegates to its `TRY` method. And of course, the `TRY/THEN` method return the continuation itself, so the calls can be chained. There is also a `DELAY` method and so we comes to the initial “Hello world”:

require('task-joose-nodejs')

TRY(function () {

    console.log('Hello')

    this.CONT.CONTINUE()

}).DELAY(1000).THEN(function () {

    console.log('world!')

}).now()

Nesting

Instead of calling the `CONTINUE` of the inner continuation, you can add a nested continuation – using `TRY` as usual. Keep in mind, that you need to activate it with `now`. When the last task of the nested continuation will call `CONTINUE` the control flow will continue on the outer level:

require('task-joose-nodejs')

TRY(function () {

    console.log('Hello')

    this.CONT.DELAY(500).THEN(function () {

        console.log(', ')

        this.CONT.CONTINUE()

    }).DELAY(500).THEN(function () {

        this.CONT.CONTINUE('world')

    }).now()

}).THEN(function (str) {

    console.log(str)

}).now()

Pretty intuitive, don’t you think? Important note: once you’ve nested the continuation (instead of calling the `CONTINUE`) you must not call its `CONTINUE` method to avoid the split of the control flow. From this point, its the nested continuation responsibility to return the control flow.

There is also a `RETURN` method to skip the other tasks from the current nesting level and return the control flow to the outer.

Exceptions

We talked about the correspondence to the `try/catch/finally` block, and here’s their equivalent in the CPS. Surprisingly, they are named `TRY/CATCH/FINALLY` :) . Simplest case:

require('task-joose-nodejs')

TRY(function () {

    throw "oops"

}).CATCH(function (e) {

    console.log("Exception caught: %s", e)

    this.CONT.CONTINUE()

}).FINALLY(function () {

    console.log("Cleanup happens")

}).now()

More text in bold: The `CATCH/FINALLY` tasks are also “continued”. That is, to return the control flow from them you need to use `this.CONT.CONTINUE()` as in `TRY/THEN`. This will allow you to write asynchronous exceptions handlers.

Nesting and Exceptions

The exceptions will correctly propagate from the nested continuations:

require('task-joose-nodejs')

TRY(function () {

    console.log('Hello')

    this.CONT.DELAY(500).THEN(function () {

        console.log(',')

        throw "world"

    }).DELAY(500).THEN(function () {

        console.log("YOU SHOULD N'T SEE THIS TEXT")

    }).now()

}).CATCH(function (str) {

    console.log(str)

}).now()

they could be re-thrown just fine:

require('task-joose-nodejs')

TRY(function () {

    console.log('Hello')

    this.CONT.DELAY(500).THEN(function () {

        console.log(',')

        throw "f$$k"

    }).CATCH(function (str) {

        throw "polite"

    }).now()

}).CATCH(function (str) {

    console.log(str, "world")

}).now()

And in general, behaves very much like “usual” exceptions.

Show me the parallel

Of course any library for asynchronous control flow should provide the “parallel” mode. In JooseX.CPS you can activate it with `AND`:

require('task-joose-nodejs')

TRY(function () {

    console.log('Activated branch 1')

    var CONTINUE = this.CONT.getCONTINUE()

    setTimeout(function () {

        console.log('world')

        CONTINUE('!')

    }, 1000)

}).AND(function () {

    console.log('Activated branch 2')

    var CONTINUE = this.CONT.getCONTINUE()

    setTimeout(function () {

        console.log('Hello')

        CONTINUE('!')

    }, 500)

}).THEN(function (res1, res2) {

    console.log(res1[0], res2[0])

}).now()

If you’ll run this code, you’ll  see that branches were activated simultaneously, but the 2 final exclamation characters appears only after the branches gets merged. The very 1st task following the `AND` group receives the `arguments` objects passed to `CONTINUE` in the order of branches declaration.

Of course, you can nest a new continuation into any of the branches:

require('task-joose-nodejs')

TRY(function () {

    console.log('Activated branch 1')

    this.CONT.CONTINUE('Branch 1 result')

}).AND(function () {

    console.log('Activated branch 2')

    this.CONT.DELAY(500).THEN(function () {

        console.log('The very long')

        this.CONT.CONTINUE()

    }).DELAY(500).THEN(function () {

        console.log('asynchronous task')

        this.CONT.CONTINUE()

    }).DELAY(500).THEN(function () {

        console.log('from several steps')

        this.CONT.CONTINUE('Branch 2 result')

    }).now()

}).THEN(function (res1, res2) {

    console.log(res1[0], res2[0])

}).now()

Some sugar

JooseX.CPS.Continuation also has lower-cased synonyms for the most used methods:

  • THEN – then
  • AND – and
  • CATCH – except
  • FINALLY – ensure

Also, the `this.CONT.THEN( … ).now()` can be written as `this.CONT.andThen( … )`

These synonyms simplifies a code a little bit and makes it more readable, but it still contain a lot of boilerplate. This boilerplate can be greatly reduced when using JooseX.CPS trait, which adds the “continued” methods to your class.

It will be described in the part 2 of the tutorial, stay tuned!

Written by Nickolay Platonov

February 14th, 2011 at 8:17 am

Posted in Joose,JooseX

Tagged with ,