If links in this document aren't work correctly, try to open it from here
JooseX.CPS - Implementation of the Continuation Passing Style1 for JavaScript, plus some syntax sugar, simplifying its usage in Joose methods and method modifiers
Stand-alone usage:
UI.maskScreen("Please wait")
TRY(function (url, data) {
XHR.request({
url : url,
data : 'data',
callback : this.CONT.getCONTINUE(),
errback : this.CONT.getTHROW()
})
}).THEN(function (response) {
if (response.isOk) {
alert('Saved correctly')
this.CONT.CONTINUE()
} else
throw 'still got the error' // or: this.CONT.THROW('still got the error')
}).CATCH(function (e) {
alert('Error during saving: ' + e)
this.CONT.CONTINUE()
}).FINALLY(function () {
UI.removeScreenMask()
}).NOW('http://remote.site.com/webservice', 'some data')
The same in OOP:
Class("DataStore", {
trait : JooseX.CPS,
has: {
data : { is: "rw" }
},
continued : {
methods : {
save : function (url) {
XHR.request({
url : url,
data : this.getData(),
callback : this.getCONTINUE(),
errback : this.getTHROW()
})
}
}
}
})
var store = new DataStore({
data : [ 1, 2, 3 ]
})
UI.maskScreen("Please wait")
store.save('http://remote.site.com/webservice').THEN(function (response) {
if (response.isOk) {
alert('Saved correctly')
this.CONTINUE()
} else
throw 'still got the error' // or: this.CONT.THROW('still got the error')
}).CATCH(function (e) {
alert('Error during saving: ' + e)
this.CONTINUE()
}).FINALLY(function () {
UI.removeScreenMask()
}).NOW()
The latest released tarball is available for downloading from http://search.npmjs.org/#/joosex-cps.
From npm
:
> [sudo] npm install joosex-cps
Setup for NodeJS:
// this extension is bundled into the following package
require('task-joose-nodejs')
Setup for browsers (assuming you've completed the 3.1 item from this document):
<script type="text/javascript" src="/jsan/Task/JooseX/CPS/Core.js"></script>
JooseX.CPS
is a trait for meta-classes, which enables "Continuation passing style" in Joose methods and method modifiers.
JooseX.CPS
allows you to define special "continued" methods and method modifiers, which forms the asynchronous interface of your class,
and behave just like ordinary methods - can be inherited, composed from Role, etc.
This module can be used on its own, completely separately from Joose. For this mode, include only the lib/Task/JooseX/CPS/Standalone.js
file in your deployment.
This file will export TRY
function:
JooseX.CPS.Continuation TRY(Function func, Object scope?, Array args?)
Create and setup an anonymous JooseX.CPS.Continuation instance.
For the complete list of the available methods please refer to the link, in the meantime, take a look on the example below to see the main idea:
TRY(function () {
var CONT = this.CONT
XHR.request({
url : url,
data : this.data,
callback : this.CONT.getCONTINUE(),
errback : function (err1, err2) {
CONT.THROW(err1, err2)
}
})
}).CATCH(function (e1, e2) {
...
this.CONT.CONTINUE()
}).FINALLY(function () {
...
this.CONT.CONTINUE()
}).THEN(function(res1, res2) {
...
}).THEN(
...
)
As you can see, the control flow of the functions, wrapped with TRY/CATCH/FINALLY
isn't managed by the standard return
statement or explicit function end.
Instead, to transfer the flow, you need to explicitly call the method on the embedded continuation instance, which is available as this.CONT
(this
scope can be passed as 2nd argument to TRY).
The call to such "control flow method" don't have to be synchronous - you can defer it arbitrary. This naturally allows to use them as callbacks (or errbacks).
However, the embedded continuation instance is only valid on the "synchronous interval" of the function execution. If you are deferring the call to the control flow method,
either capture the continuation to the closure, or use one of the getCONTINUE/getTHROW/getRETURN
methods (see the example above).
The arguments to the control flow method will become arguments to the next corresponding section of the flow. If the scope was passed as 2nd argument, it will propagate as the scope of all further statements at the same nesting level.
// CONTINUE transfers to next THEN section
var tryScope = {}
TRY(function (p1) {
// p1 == 'p1'
// this == tryScope
this.CONT.CONTINUE('value1', 'value2')
}, tryScope).THEN(function(arg1, arg2) {
// arg1 == 'value1', arg2 == 'value2'
// this == tryScope
this.CONT.CONTINUE('value3', 'value4')
}).THEN(function(arg1, arg2) {
// arg1 == 'value3', arg2 == 'value4'
// this == tryScope
}).NOW('p1')
// THROW transfers to the corresponding CATCH section
var tryScope = {}
var catchScope = {}
TRY(function (p1) {
// p1 == 'p1'
// this == tryScope
this.CONT.THROW('error1', 'error2')
}).CATCH(function(arg1, arg2) {
// arg1 == 'error1', arg2 == 'error2'
// this == catchScope
this.CONT.CONTINUE('value3', 'value4')
}, catchScope).FINALLY(function() {
// this == tryScope
this.CONTINUE()
}).THEN(function(arg1, arg2) {
// this == tryScope
// arg1 == 'value3', arg2 == 'value4'
}).NOW('p1')
-
You can't return the value from the statement's function, using the standard
return
. Any value returned will throw an exception. -
The
CATCH/FINALLY
statements are also "continued" - they have embedded continuation instance, and do not transfer the control flow without call toCONTINUE
(or other method). If you'll forget to call it - your control flow will remain inside of theCATCH/FINALLY
statement -
Arguments to
CONTINUE
from inside theCATCH
statement will be passed further on flow - to the nextTHEN
section (this may change in future versions) -
Arguments to
CONTINUE
from inside theFINALLY
statement will be ignored. -
You can pass the scope for the statement's function as the 2nd argument of any control flow method
-
You can pass the arguments for the statement's function as the 3rd argument of any control flow method. This will override the arguments received from the previous statement (or from
NOW
, see below).
The control flow, defined with the TRY/CATCH/FINALLY
will not start immediately. To start it, call the NOW
method of the continuation.
Arguments to NOW
will become the arguments to the initial statement (see the example above).
Alternatively, there is a andTHEN
method of the continuation (not related to parallel flow).
Its equivalent to .THEN().NOW()
. Note, that when using this method the NOW
method will be called w/o arguments.
You can nest the statements arbitrary. To do it, instead of global TRY
use the TRY
method of the embedded continuation:
TRY(function () {
this.CONT.TRY(function () {
XHR.request({
url : url,
data : this.data,
callback : this.CONT.getCONTINUE(),
errback : this.CONT.getTHROW(),
})
}).THEN(function () {
// do something else
}).NOW()
}).CATCH(function (e1, e2) {
...
this.CONT.CONTINUE()
}).FINALLY(function () {
this.CONT.TRY(function () {
...
this.CONT.CONTINUE()
}).NOW()
}).THEN(function(res1, res2) {
...
}).THEN(
...
)
To raise the exceptions you have 2 options. If you are raising it, during the "synchronous interval" of the statement's function, you can just use the standard throw
:
TRY(function (arg1, arg2) {
if (!arg1 || !arg2) throw new MyException({ description : 'Incorrect arguments' })
...
}).CATCH(function (e) {
this.CONT.CONTINUE()
}).FINALLY(function () {
...
}).NOW()
If you are raising it deferred, use the THROW
method of the continuation:
TRY(function (arg1, arg2) {
var CONT = this.CONT
XHR.request({
errback : function (err1, err2) {
CONT.THROW(new MyException({ param1 : err1, param2 : err2 }))
}
})
}).CATCH(function (e) {
this.CONT.THROW(e)
}).FINALLY(function () {
...
}).NOW()
Note, that with THROW
you can throw (and accordingly catch) several values. If you are deferring call to THROW
, you should use getTHROW
(or a closure).
Exceptions from the nested statements will be correctly caught/handled by the outer CATCH
statements. All FINALLY
statements will be honored, in the correct order.
You can re-throw the exceptions from CATCH/FINALLY
statements, to propagate it.
When defining your control flow with TRY/THEN/CATCH/FINALLY
, consider the following rules, which affect the scope of CATCH/FINALLY
statements.
-
THEN
is just a synonym forTRY
. Calls toTRY/THEN
add statements to the sequential group. -
CATCH/FINALLY
will handle the exceptions from the immediate previous sequential group -
THEN
afterCATCH/FINALLY
will start a new sequential group -
If you need to explicitly start a new sequential group use
NEXT
Take a look on some illustrating examples:
TRY(function () { | try {
| doSomething1()
doSomething1() | doSomething2()
| } catch (e) {
}).THEN(function() { |
| doCatch()
doSomething2() |
| } finally {
}).CATCH(function() { | doFinally()
| }
doCatch() |
|
}).FINALLY(function () { |
|
doFinally() |
}) |
TRY(function () { | doSomething1()
|
doSomething1() | try {
| doSomething2()
}).NEXT(function() { |
| } catch (e) {
doSomething2() | doCatch()
| }
}).CATCH(function() { |
| try {
doCatch() |
| doSomething3()
}).THEN(function (){ |
| } finally {
doSomething3() | doFinally()
| }
}).FINALLY(function () { |
|
doFinally() |
}) |
In any statement of the sequential group, you can also skip the rest of statements, using RETURN
TRY(function (arg) { | (function() {
| if (!arg) return 'value1'
if (!arg) this.CONT.RETURN('value1') |
| doSomething1()
doSomething1() | doSomething2()
| })()
}).THEN(function() { |
| doSomething3()
doSomething2() |
|
}).NEXT(function(arg) { |
// arg == 'value1' |
|
doSomething3() |
}) |
Remember, if you are deferring the call to RETURN
, either capture the continuation to closure, or use getRETURN
Along with sequential groups, you can run your flow in parallel, with the AND
method of the continuation.
Please note, that AND
will change the type of the whole current group to parallel. So all previous calls to THEN
in the current group will be considered
as parallel branches (this may change in future versions). But, after the type of current group has been changed to parallel, calls to THEN
will start a new group
(you can always start a new sequential group with NEXT
).
When entering the parallel group, all branches will receive the same arguments:
TRY(function (p) { | NOW('param')
//branch1, p == 'param' | |
| |
| /------------------------\
this.CONT.CONTINUE('res1') | | | | |
| | | | |
}).THEN(function(p) { | branch1 branch2 branch3 branch4
//branch2, p == 'param' | | | | |
| | | | |
this.CONT.CONTINUE('res2') | \------------------------/
| |
}).AND(function(p) { | |
//branch3, p == 'param' | THEN( [1], [2], [3], [4] )
|
this.CONT.THROW('err3') |
|
}).AND(function(p) { |
//branch4, p == 'param' |
|
this.CONT.THROW('err4') |
|
}).THEN(function (r1, r2, r3, r4) { |
|
// r1 == [ 'res1' ] |
// r2 == [ 'res2' ] |
// r3 == [ 'err3' ] |
// r4 == [ 'err4' ] |
|
}).NOW('param') |
|
The synchronization point will receive several arguments, by the number of branches. Each argument will be the arguments
object with the results of each branch.
Arguments be filled in order of branches declaration, not in the order they finished execution.
The following convention may change in future versions: Each AND
statement is implicitly wrapped with CATCH
. So the whole parallel group will never
throw an exception (adding a CATCH
statement to it will be a no-op, though FINALLY
statement will be honored). Instead, any exceptions from branches will be caught,
and considered as the results. Its your responsibility to examine the resulting array and separate exceptions from normal results.
The following upper-case methods of the continuation have lower-case synonyms:
THEN | then
andTHEN | andThen
NEXT | next
AND | and
NOW | now
CATCH | except
FINALLY | ensure
For this mode, include the lib/Task/JooseX/CPS/All.js
file in your deployment.
Adding JooseX.CPS
trait will provide your class with the continued
builder. This builder groups the declarations of the "asynchronous part" of your class.
Inside of it, you can use the following builders: methods
, override
, after
, before
. This builders have the same meaning as standard ones, however instead of usual,
they define the "continued" methods.
These methods are called "continued", because they are implicitly wrapped with the "continued" TRY
statement, described in the section above.
Inside of such methods, there is also embedded continuation instance, available as this.CONT
Class("DataStore", {
trait : JooseX.CPS,
continued : {
methods : {
save : function (url) {
if (!url.test(/^http/) throw "Invalid URL"
...
this.CONTINUE(result)
}
}
}
})
Continued methods have the same [features][] as functions, wrapped with TRY
, plus some important additional ones, see below.
The class with JooseX.CPS
trait, will receive a JooseX.CPS.ControlFlow
role, which will provide several shortcut methods:
TRY/THEN/AND/NEXT
CATCH/FINALLY
THROW/CONTINUE/RETURN
getCONTINUE/getTHROW/getRETURN
All this methods just delegates to the embedded continuation instance.
"Continued" methods supports implicit methods chaining, and this allows you to write asynchronous code, that looks almost like synchronous one.
Consider the following example and its "raw" equivalent:
Class("Data.Store", { | var store = new Data.Store()
trait : JooseX.CPS, |
| // the "de-sugared" version of `update` method
continued : { | TRY(function (url) {
|
methods : { | this.CONT.TRY(function () {
| //do save
log : function () { | this.CONT.CONTINUE()
//do log | }).THEN(function () {
this.CONTINUE() | //do log
}, | this.CONT.CONTINUE()
| }).NOW()
save : function (url) { |
//do save | }, store, [ 'http://my.site.com' ]).NOW()
this.CONTINUE() |
}, |
|
update : function (url) { |
this.save(url) |
|
this.log().now() |
} |
} |
} |
}) |
|
var store = new Data.Store() |
|
store.update('http://my.site.com').now() |
Technically, each call to "continued" method will pick up the current continuation, and delegate to its TRY
method.
Thus, the sequential method calls will form a sequential statements group.
This will work for you as long as your whole method form a single source of exceptions. If you need a fine-tuned control over the areas from where the exceptions may came from, then fall-back to the usual control flow methods chaining.
For example, if you need this "exceptions layout" in your method:
update : function (url) {
this.save(url)
try {
this.log()
} catch (e) {
}
}
then write it like:
update : function (url) {
this.save(url).next(function () {
this.log().now()
}).except(function () {
this.CONTINUE()
}).now()
}
Looks a bit noisier, but you can't get something for nothing, don't you?
The implicit methods chaining also works for the case, when you call "continued" methods of different objects. This works because single-threaded nature of JavaScript allows us to keep the global continuation instance. For example:
Class("Data.Logger", {
trait : JooseX.CPS,
continued : {
methods : {
log : function () {
//do log
this.CONTINUE()
}
}
}
})
Class("Data.Store", {
trait : JooseX.CPS,
has : {
logger : null,
},
continued : {
methods : {
save : function (url) {
//do save
this.CONTINUE()
},
update : function (url) {
this.save(url)
this.logger.log().now()
}
}
}
})
var store = new Data.Store()
store.update('http://my.site.com').now()
Thing to note is that, the scope propagates along the statements chain and your should consider that. So, if in your "continued" method, you invoke the "continued" method of the another object,
the scope of the following CATCH/FINALLY/THEN/NEXT
statements will be that object. To override it, pass 2nd argument to control flow method.
Take a look on the illustrating examples:
update : function (url) { | update : function (url) {
this.save(url) | this.save(url)
|
var me = this | var me = this
var logger = this.logger | var logger = this.logger
|
logger.log().then(function () { | logger.log().andThen(function () {
|
// this == logger | // this == me
|
}).now() | }, this)
} | }
You can define the "continued" method modifiers as well. The only modifier type, that isn't supported is augment
.
All other types works identically to their normal variants, just keep in mind that you need to explicitly call CONTINUE
(or THROW
)
to transfer the control flow from the modifier. In the same way, when using override
don't forget that call to this.SUPER()
will not start
the method immediately, you need to activate it: this.SUPER().now()
Class("Data.Store.Improved", {
trait : JooseX.CPS,
continued : {
override : {
log : function (arg) {
if (arg == 'something')
this.SUPER().now()
else
throw "Wrong arguments"
}
},
methods : {
save : function () {
...
this.CONTINUE()
}
},
after : {
save : function () {
...
this.CONTINUE()
}
}
}
})
JooseX.CPS
even allows you to freely mix the usual and "continued" methods in override/after/before
modifiers.
That is, if your superclass defines a usual method, you can re-define it into the "continued" section just fine. The same about the modifiers. Naturally, after such operation, your method will became "continued".
The vice-versa re-definition is also valid.
This extension is supported via github issues tracker: http://github.com/SamuraiJack/JooseX-CPS/issues
You can also ask questions at IRC channel : #joose
Or the mailing list: http://groups.google.com/group/joose-js
Continuation class: JooseX.CPS.Continuation
Web page of this extension: http://github.com/SamuraiJack/JooseX-CPS/
General information about Joose: http://joose.it
All complex software has bugs lurking in it, and this module is no exception.
Please report any bugs through the web interface at http://github.com/SamuraiJack/JooseX-CPS/issues
Nickolay Platonov nplatonov@cpan.org
Copyright (c) 2009, Nickolay Platonov
All rights reserved.
Redistribution and use in source and binary forms, with or without modification, are permitted provided that the following conditions are met:
- Redistributions of source code must retain the above copyright notice, this list of conditions and the following disclaimer.
- Redistributions in binary form must reproduce the above copyright notice, this list of conditions and the following disclaimer in the documentation and/or other materials provided with the distribution.
- Neither the name of Nickolay Platonov nor the names of its contributors may be used to endorse or promote products derived from this software without specific prior written permission.
THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.