function concat(x, y) {
return '' + x + y;
}
function capitalize(x) {
return x.toUpperCase();
}
var result = concat("Pi is exactly ", 3);
var capitalized = capitalize(result);
console.log(capitalized + '!');
Flow is lineal, function nesting keeps low
Control is returned to the main program over and over after exiting a function
function concat(x, y, continuation) {
var result = '' + x + y;
continuation(result);
}
function capitalize(x, continuation) {
var result = x.toUpperCase();
continuation(result);
}
concat("Pi is exactly ", 3, function nowCapitalizeAndPrint (result) {
capitalize(result, function nowPrint (capitalized) {
console.log(capitalized + '!');
});
});
Flow is chained, function nesting increases with each call
Control is explicitely defined as the continuation parameter
Why callbacks are continuations?
function get(url, onsuccess, onerror) {
var xnr = new XMLHttpRequest();
xhr.open('GET', url, true); // send asynchronously!
xhr.send(null);
xhr.onreadystatechange = function () {
if (xhr.readyState !== 4)
return;
if (xhr.status === 200)
onsuccess(xhr.responseText);
else if (typeof onerror === 'function')
onerror(xhr.responseText);
}
}
The get()
function is written in CPS with two continuations, one in case of success and other in case of error
Wait a moment. What is a continuation?
function get(url, continuation) { // <-- this is a function in CPS
// Do some async stuff
}
function nowShow(result) { // <-- this is a continuation
console.log(JSON.stringify(result));
}
get('http://calc.org/functions/', nowShow); // <-- this is call in CPS
Let's write CPS manually
return
explicitelyContinuations could be annoying. Try converting this:
var a = 1, b = 4, c = 3;
console.log((a+Math.sqrt(b*b-4*a*c))/(2*a));
into CPS...
// Suppose these operators
function add(x, y, cont) { cont(x+y); }
function mul(x, y, cont) { cont(x*y); }
function div(x, y, cont) { cont(x/y); }
function sqr(x, cont) { cont(Math.sqrt(x); }
// Compute acum = mul(b, b)
// Then compute acum2 = mul(-4, a)
// Then compute acum2 = mul(acum2, c)
// Then compute acum2 = add(acum, acum2)
// Then compute acum2 = sqr(acum2)
// Then compute acum2 = add(a, acum2)
// Then compute acum3 = mul(2, a)
// Then compute acum3 = div(acum2, acum3)
// Then console.log(acum3)
Here it is the Pyramid of Doom
mul(b, b, function (acum) {
mul(-4, a, function (acum2) {
mul(acum2, c, function (acum2) {
add(acum, acum2, function (acum2) {
sqr(acum2, function (acum2) {
add(a, acum2, function (acum2) {
mul(2, a, function (acum3) {
div(acum2, acum3, function (acum3) {
console.log(acum3);
});
});
});
});
});
});
});
});
But now suppose these alternative implementations...
function add(x, y, cont) {
get('http://calc.org/add/' + x + '/' + y, cont);
}
function mul(x, y, cont) {
get('http://calc.org/mul/' + x + '/' + y, cont);
}
function div(x, y, cont) {
get('http://calc.org/div/' + x + '/' + y, cont);
}
function sqr(x, cont) {
get('http://calc.org/sqr/' + x, cont);
}
Now CPS allow us to compute the formula in the server, in an asynchronous fashion and without blocking the interface
How about supporting tail call optimization?
Or writing non-blocking tasks?
Even coordinating asynchronous functions?
setTimeout
to reset the stack!This code produces an error in Firefox
function sum(n) {
if (!n)
return 0;
return n + sum(n-1);
}
console.log(sum(100000));
InternalError: too much recursion
Its CPS form hangs Firefox console as well, :(
function sum(n, acum, continuation) {
if (!n)
continuation(acum);
else
sum(n-1, n+acum, continuation);
}
sum(100000, 0, console.log);
InternalError: too much recursion
var skip = 0;
function sum(n, acum, continuation) {
if (!n)
setTimeout(function () { continuation(acum); });
// Reset stack every 1000 calls
else if (skip++ % 1000 === 0)
setTimeout(function () { sum(n-1, n+acum, continuation); });
else
sum(n-1, n+acum, continuation);
}
sum(100000, 0, console.log);
5000050000
setTimeout()
forces the function to return clearing the call stack and scheduling the next call to be executed ASAP
Try this on:
http://www.whatwg.org/specs/web-apps/current-work/
A real pain to convert into CPS, x(
function traverse(node, f) {
// A- Call f on the node
f(node);
// B- Get the children
var children = node.childNodes;
for (var i=0, len=children.length; i<len; i++) {
traverse(children[i], f);
}
}
Easy to convert in CPS, xP
var stats = {};
function histogram(node, continuation) {
if (node.nodeType === 3)
node.nodeValue.replace(/\w+/g, function(match) {
stats[match] = stats[match] ?
stats[match] + 1 : 1;
return match;
});
}
A paradigmatyc example, x):
function show() {
var keys = Object.keys(stats);
for (var i=0; i < keys.length; i++) {
var key = keys[i];
console.log(key + ': ' + stats[key]);
}
}
function show() {
var keys = Object.keys(stats);
var i = 0; // Initialization
while (i < keys.length) { // Condition
var key = keys[i];
console.log(key + ': ' + stats[key]);
i++; // Increment
} // Nothing more remains
}
The invocation:
traverse(document.body, histogram);
show();
Now into CPS...
The simplest one
var stats = {};
function histogram(node, continuation) {
if (node.nodeType === 3)
node.nodeValue.replace(/\w+/g, function(match) {
stats[match] = stats[match] ?
stats[match] + 1 : 1;
return match;
});
// Just call the continuation at the end
continuation();
}
The paradygmatic example: a for
loop
function show(continuation) {
function handleOne(i, keys, continuation) {
if (i < keys.length) { // Condition
var key = keys[i];
handleOne(i+1, keys, continuation); // Increment
}
else {
continuation(); // Continuation is what to do after
}
}
// --> STARTS HERE <--
handleOne(0, Object.keys(stats), continuation); // Initialization
}
The tough guy: recursion
function traverse(node, f, continuation) {
f(node, function handleChildren() { // A- Call f on the node
var children = node.childNodes; // B- Get the children
function handleOne(i, len, continuation) {
if (i < len) // Condition
traverse(children[i], f, function handleNext() {
handleOne(i+1, len, continuation); // Increment
});
else
continuation(); // Continuation is what to do after
}
// --> STARTS HERE <--
handleOne(0, children.length, continuation) // Initialization
});
}
The invocation
function end() {}
traverse(document.body, histogram, function () {
show(end);
});
But this implementation introduce soo much recursion
So let's restart the stack...
var stats = {};
function histogram(node, continuation) {
if (node.nodeType === 3)
node.nodeValue.replace(/\w+/g, function(match) {
stats[match] = stats[match] ?
stats[match] + 1 : 1;
return match;
});
// Restart the stack every 400 nodes (give up for 10ms)
if ((histogram.skip++ % 400) === 0) {
console.log('400 nodes processed');
setTimeout(continuation, 10);
} else
continuation();
}
histogram.skip = 0;
And allow other stuff to get control from time to time
function show(continuation) {
function handleOne(i, keys, continuation) {
if (i < keys.length) {
var key = keys[i];
console.log(key + ': ' + stats[key]);
// Stop processing for 1s (you can not read so fast)
if ((i % 20) === 0)
setTimeout(function() {
handleOne(i+1, keys, continuation);
}, 1000);
else
handleOne(i+1, keys, continuation);
} else {
continuation();
}
}
handleOne(0, Object.keys(stats), continuation);
}
sync()
function to flat the pyramidsync(
// Format: [function, param1, param2, ..., where to store result]
[get, 'http://mycompany.com/employees', 'result'],
[filterByFunction, result, 'developer', 'filtered'],
[show, filtered, '']
);
sync()
takes several async functions to be synchronized
But be careful! result
and filtered
are both undefined
at the time they are evaluated
sync(
// Format: [function, "param1, param2, ...", where to store result]
[get, "'http://mycompany.com/employees'", 'result'],
[filterByFunction, "result, 'developer'", 'filtered'],
[show, "filtered", '']
);
Wrap list of parameters into a string
Now they are not evaluated, they are only text
To unwrap, build an array-like string...
var parameters_array = '[' + parameters_string + ']'
...and safetly evaluate them using with
and eval
with (some_namespace)
var parameters = eval(parameters_array);
function sync() {
// Convert arguments into an array
var args = [].slice.call(arguments);
// Provide a namespace to store results
var namespace = {};
// next() computes the continuation
var f, parameters, store_name;
function next(result) {
// Functions remain so...
if (args.length) {
// Store the result of former one into the namespace
if (store_name)
namespace[store_name] = result;
// Take the next function
var tuple = args.shift();
f = tuple[0];
// Take parameters from the namespace
with (namespace)
parameters = eval('['+tuple[1]+']');
// Take where to store the next result
store_name = tuple[2];
// Add the continuation to the parameters of the function
parameters.push(next);
// Call the function
f.apply(null, parameters);
}
}
next();
}
Let's flat that doomed pyramid...
sync(
[mul, "b, b", 'acum'],
[mul, "-4, a", 'acum2'],
[mul, "acum2, c", 'acum2'],
[add, "acum, acum2", 'acum2'],
[sqr, "acum2", 'acum2'],
[add, "a, acum2", 'acum2'],
[mul, "2, a", 'acum3'],
[div, "acum2, acum3", 'acum3'],
[console.log, "acum3", '']
);
I saw the term Pyramid of Doom for the first time in Why coroutines won't work on the web by Dave Herman