StratifiedJS
JavaScript + structured concurrency.
Bring structure to your JavaScript based asynchronous applications.
A small but powerful extension
to the language of the web.
Contents
SJS Constructs
Introduction
StratifiedJS extends the JavaScript language with a small number of constructs for concurrent programming. It allows you to express asynchronous control flow in a straightforward sequential style:
var news;
waitfor {
news = http.get("http://news.bbc.co.uk");
}
or {
hold(1000);
news = http.get("http://news.cnn.com");
}
or {
hold(1000*60);
throw "sorry, no news. timeout";
}
show(news);
This snippet
- tries to retrieve the news from the BBC.
- concurrently tries CNN if news not received after 1 second.
- displays the first news that comes in, and cancels any request that might still be pending.
- times out after 1 minute if no news received, automatically cancelling the pending requests.
For more information about the rationale and basic usage of StratifiedJS, please see one of these interactive presentations from recent talks:
- Emerging Languages Camp, OSCON Portland 2010
- AmsterdamJS X-Mas Special 2010
- University Ghent, April 2011
- Cologne.js JavaScript Meetup, May 2011
Implementations
The reference implementation of StratifiedJS is Oni Apollo. It is an open-source implementation for use in browsers (including IE6+ and mobile) and on the server (based on node.js). Apollo is written in JavaScript and hence requires no browser extension or plugins.
We're also investigating extending Google's V8 JavaScript engine with native StratifiedJS support.
Language Overview
StratifiedJS executes code in logical units we call strata. Unlike normal JS code, strata are allowed to "block", i.e. the control flow is allowed to stop at a particular point to be picked up again later at the same point where it left off. An example would be the hold statement which blocks for a given period of time.
While one stratum is blocked, other strata can execute. Strata are a bit like threads, but they are much more deterministic: Only one stratum is running at any one time and it is executed atomically up until the point where it either finishes or suspends. At that point the next pending stratum is executed until it finishes or suspends, and so forth.
StratifiedJS contains constructs for casting asynchronous logic into
blocking form (waitfor()) and constructs for combining multiple strata (waitfor/and, waitfor/or, spawn) in a structured way.
Pausing execution
StratifiedJS's hold function can be called with a number argument t to suspend the current stratum for ~t milliseconds:
... hold(1000); // the next code will be executed after around 1s ...
Note that hold only suspends the stratum that it appears in; it doesn't block the whole program and it doesn't 'busy-wait'. Other concurrent strata can continue to execute during this time. If you are running SJS in a browser, the UI will stay fully responsive during periods of suspension.
Calling hold with an undefined argument suspends the current stratum indefinitely.
Creating new strata
StratifiedJS offers several different constructs for running multiple concurrent strata.
One of the most straightforward ways is to use StratifiedJS's spawn operator to execute some code in the background, similar to how you would "spawn a thread" in other programming languages:
spawn subexpression;
This spawn expression executes subexpression synchronously until subexpression suspends or finishes, and then returns a stratum object:
| Class Stratum | |
|---|---|
abort |
Function that aborts the spawned stratum (if it is not finished yet). |
waitforValue |
Function that returns the value of the spawned stratum expression. If the stratum isn't finished yet, blocks until it is. If the stratum threw an exception, waitforValue throws this exception. If the stratum was aborted (through a call to Stratum.abort), waitforValue throws an exception too. |
waiting |
Function that returns the number of strata currently waiting for the spawned stratum to finish by being blocked in waitforValue (requires Apollo >=0.12). |
Further execution of subexpression proceeds asynchronously until it finishes or is aborted through a call to Stratum.abort.
Instead of spawning a stratum it is often a better idea to use one of
the structured concurrency constructs waitfor/and and waitfor/or - see below.
Note: The section above describes the spawn construct as it is implemented in recent versions of StratifiedJS. In older versions (Apollo < 0.11), spawn took a function argument instead of an expression.
Parallel composition
Parallel composition or 'fork-join' composition is expressed with
SJS's waitfor/and construct:
waitfor {
... some code ...
}
and {
... some other code ...
}
... next code ...
This code executes some code until it finishes or suspends. It then
executes some other code until the latter finishes or suspends. Only
when both some code and some other code have finished will the
execution proceed with next code.
waitfor/and can take more than 2 clauses, as well as optional catch/retract/finally clauses (like try). The full syntax is:
waitfor block1 and block2 [ and block3 [ and ... ] ] [ catch(e) catch_block ] [ retract retract_block ] [ finally finally_block ]
Alt composition
Alt ('alternatives') composition explores a set of alternatives in
parallel. It is expressed with SJS's waitfor/or construct:
waitfor {
... some code ...
}
or {
... some other code ...
}
... next code ...
This code first executes some code. If some
code finishes, then
some other code will be skipped, and execution proceeds
directly with next code. If, instead, some
code suspends, some other code will be executed
until it either suspends or finishes. When either some
code or some other code finishes, while the other is
suspends, the suspended code will be cancelled.
One scenario where alt composition is helpful is to add timeouts:
waitfor {
var rv = do_request_to_database_server();
}
or {
hold(1000);
throw ("timeout in database server communication");
}
Here, we're timing out our database request after 1s. Note how we can
treat do_request_to_database_server as a complete black box. SJS
is modular!
waitfor/or can take more than 2 clauses, as well as optional catch/retract/finally clauses (like try). The full syntax is:
waitfor block1 or block2 [ or block3 [ or ... ] ] [ catch(e) catch_block ] [ retract retract_block ] [ finally finally_block ]
Collapsing alternatives
The collapse keyword (requires Apollo >=0.13) can be used to prematurely collapse the strata in a waitfor/or construct:
waitfor {
console.log('a1');
hold(1000);
console.log('a2');
}
or {
console.log('b1');
collapse; // this aborts all other waitfor..or clauses
hold(2000);
console.log('b2');
}
// This code prints a1, b1, b2.
Suspending execution
Using SJS's waitfor() construct, called the suspending waitfor (not to be confused with the composition constructs waitfor/and/or), execution of a stratum can be
suspended until explictly resumed:
waitfor() {
... "suspending code block" ...
}
... next code ...
Here, the suspending code block (the code inside the curly brackets)
will be executed and the stratum will suspend. next code will not be
executed until the stratum is explicitly resumed. For this purpose,
the waitfor() construct defines a function resume inside its suspending
code block. Calling resume() will resume the corresponding suspended
stratum at the point of next code. Only the first call to resume
will have an effect (and only if the suspended waitfor has not been
cancelled). Subsequent calls to resume() will be ignored.
Example: In a web browser we can use waitfor() with window.setTimeout
to make a 'pause' function (similar to hold(t)):
function pause(t) {
waitfor() {
window.setTimeout(resume, t);
}
}
Parameters passed into the call to resume() can be captured by putting
them into the waitfor()'s variable list:
waitfor(var a, b, c) {
window.r = resume;
}
console.log("a,b,c = "+a+","+b+","+c);
//code running in another stratum:
...
window.r(1,2,3);
...
This code will log "a,b,c=1,2,3".
The full syntax of the suspending waitfor construct is:
waitfor ( [return_var_decls] ) block [ catch(e) catch_block ] [ retract retract_block ] [ finally finally_block ]
Cancellation
There are a number of situations under which suspended code gets
cancelled, e.g. when another branch in a waitfor/or construct finishes
first, or when a branch in a waitfor/and returns early:
function foo() {
waitfor {
...some suspending code...
}
and {
...
return;
}
}
Here, if some suspending code is still suspended by the time of the
return call in the other branch, it will be cancelled.
Cancellation means that a suspended waitfor()-construct will not wait
for a call to its corresponding resume any more. Also, a hold(t) will
not wake up any more.
Cancellation can be "caught" much like exceptions using try/retract:
try {
... some suspending code ...
}
retract {
console.log("'some suspending code' has been cancelled!");
}
Cancellation is also honoured by try/finally; i.e. a finally clause
will be executed whether a try block is left normally, by exception or
by cancellation.
As a shorthand, catch, retract, and finally blocks can be appended
directly to a waitfor(), waitfor/and, or waitfor/or, without having to
wrap it into a try-clause.
Example: Here is a function that waits for a DOM event in the webbrowser:
function waitforEvent(elem, event) {
waitfor(var rv) {
elem.addEventListener(event, resume, false);
}
finally {
console.log("cleanup on "+elem.id+"...");
elem.removeEventListener(event, resume, false);
}
return rv;
}
Factoring out try/finally
Similar to Python's with statement
(PEP-0343), SJS has a using
statement for factoring out standard uses of try/finally:
using (context_manager) {
... some code ...
}
This code executes the code block some code. When some code is
exited (either normally, by exception or by cancellation),
context_manager.__finally__() will be called, if this function exists.
I.e. the above code is equivalent to something like:
try {
... some code ...
}
finally {
if (context_manager &&
typeof context_manager.__finally__ == "function")
context_manager.__finally__();
}
Example of an event loop in Oni Apollo:
using (var q = require("apollo:dom").eventQueue(window, "keydown")) {
for (;;) {
switch (q.get().keyCode) {
...
}
}
}
Base Language
StratifiedJS is based on the JavaScript language as defined in ECMA-262 Edition 3. In addition, StratifiedJS supports several functions defined in ECMA-262 Edition 5:
Array.isArrayArray.prototype.indexOfArray.prototype.lastIndexOfObject.keysFunction.prototype.bind
Depending on the particular browser or server environment, additional features from recent ECMA editions (such as e.g. Object.create, Object.seal, etc) might be available.
StratifiedJS also supports some of the syntactic sugar proposed for ECMA-262 Edition 6:
Destructuring Support
'Destructuring' is a convenient syntax for picking apart structured data, mirroring the way array and object literals are constructed.
E.g. if you have a function that returns an array, you can directly extract this to individual variables using the following destructuring assignment:
function foo() { return ['x', 'y', 'z']; }
var a,b,c;
[a,b,c] = foo();
// we'll now have a=='x', b=='y' and x=='z'
Similarly, objects can be picked apart like this:
function bar() { return {x:1, y:2, z:3; } }
var a,b,c;
({x:a,y:b,z:c}) = bar();
// we'll now have a==1, b==2 and x==3
Note that object patterns cannot appear at the start of a statement, hence the parenthesization of the object pattern in ({x:a,y:b,z:c}) = bar();.
Destructuring works with nested array and object data, too. Here's an example of extracting the third Google search result for "js destructuring": [Run example in sandbox]
var title, url;
({responseData:
{
results: [ , , {title, url}]
}
}) = require("apollo:google").search("js destructuring");
console.log("3rd Google result: "+ title + " --- " + url);
This pattern makes use of a DRY shorthand whereby the pattern {title, url} is equivalent to {title:title, url:url}.
The two empty elements in the pattern [ , , {title, url}] cause the assignment to skip the first two results returned by the google search, and bind title and url to the third result.
Please see the destructuring docs at ecmascript.org for full syntax details.
Note that destructuring support as implemented in Apollo is currently limited to plain assignment expressions. Destructuring in var definitions, for/in loops, catch clauses or parameter lists is not supported yet.
