Continuations in a Nutshell
Continuations are a powerful abstraction. Let me emphasize that. Continuations are an immensely powerful abstraction. Why are continuations so powerful? It's because a continuation is simply a function[1] and “functions have a dangerous property of being invokable.” More on that later.
So, if a continuation is just a function then why is it so special?
Yes, a continuation is just a function. However, what makes a continuation so special is what it represents. A continuation represents the “rest of the computation” (a.k.a. the computation context). For example, consider the following Scheme expression:
(add1 (* 3 x))
; |_____|
; |
; computation
(add1 [])
; |_______|
; |
; context
Here the computation (* 3 x)
has the context (add1 [])
where the []
represents a hole. The hole can be plugged with the result of a computation. This is written as (add1 [result])
for some result
. A continuation is just a representation of a context. For example, the function (lambda (result) (add1 result))
represents the context (add1 [])
.
On the other hand, the computation (* 3 x)
can also be represented as a function. It's represented as the function (lambda (context) (context (* 3 x)))
where context
is a continuation representing the context of the computation. In should be noted that the Cont
type in Haskell represents the computation itself, not its context.
Okay, but what makes continuations so powerful?
As I said before, a continuation is simply a function and “functions have a dangerous property of being invokable.” In particular, a function may be invoked not only once but also arbitrarily many times or even never at all. This is what makes continuations so powerful.
For example, consider the aforementioned computation (* 3 x)
represented as a function:
(lambda (context)
(context (* 3 x)))
What if we applied context
more than once? We could use it to double the result as follows:
(lambda (context)
(+
(context (* 3 x))
(context (* 3 x))))
If the given context
is add1
then this will produce the answer (* 2 (add1 (* 3 x)))
.
On the other hand, what if we never applied context
? We could short-circuit the evaluation:
(lambda (context)
(* 3 x))
This is precisely what call/cc
does. It short-circuits the evaluation by ignoring the context and simply returning an answer. For example, consider:
(call/cc (lambda (short-circuit)
(add1 (short-circuit (* 3 x)))))
Here, the computation (* 3 x)
has the context add1
. However, we short-circuited the computation by applying the context of call/cc
(i.e. short-circuit
) to the result of the computation. Hence, we ignored the original context (i.e. add1
) and returned an answer.
How is call/cc
implemented?
Now that we understand continuations let's look at the definition of callCC
in Haskell:
callCC :: ((a -> Cont r b) -> Cont r a) -> Cont r a
-- |___________________________|
-- |
-- f
callCC f = Cont $ \k -> runCont (f (\a -> Cont $ \_ -> k a)) k
-- __|___ |______________________|
-- | | |
-- (a -> r) short-circuit
It should be noted that k
is the current continuation (i.e. the continuation of the entire expression). The function f
is the only input to callCC
. It returns a Cont r a
which represents the entire computation to be performed. We apply it to k
to get the result of the computation.
However, inside the computation we can call short-circuit
whenever we want to short-circuit the evaluation. This function takes a result and returns a computation that ignores its context and applies the current continuation k
to the result, thereby short-circuiting the evaluation.
Putting it all together in JavaScript.
We understood what continuations are in Scheme. We understood how callCC
works in Haskell. Let's use our newfound knowledge to implement continuations and callCC
in JavaScript:
var Cont = defclass({
constructor: function (runCont) {
this.runCont = runCont;
},
map: function (f) {
return new Cont(k => this.runCont(x => k(f(x))));
},
apply: function (g) {
return new Cont(k => this.runCont(f => g.runCont(x => k(f(x)))));
},
bind: function (f) {
return new Cont(k => this.runCont(x => f(x).runCont(k)));
}
});
Cont.of = x => new Cont(k => k(x));
var callCC = f => new Cont(k => f(x => new Cont(_ => k(x))).runCont(k));
var log = prefix => x => console.log(prefix, x);
var add1 = x => Cont.of(x + 1);
callCC(short_circuit => short_circuit(15).bind(add1)).runCont(log("short"));
callCC(short_circuit => Cont.of(15).bind(add1)).runCont(log("no short"));
function defclass(prototype) {
var constructor = prototype.constructor;
constructor.prototype = prototype;
return constructor;
}
Beware, callCC
can be used to implement goto
.
The power of callCC
allows you to create arbitrary control flow structures like throw
, coroutines and even goto
as can be seen here:
var Cont = defclass({
constructor: function (runCont) {
this.runCont = runCont;
},
map: function (f) {
return new Cont(k => this.runCont(x => k(f(x))));
},
apply: function (g) {
return new Cont(k => this.runCont(f => g.runCont(x => k(f(x)))));
},
bind: function (f) {
return new Cont(k => this.runCont(x => f(x).runCont(k)));
}
});
Cont.of = x => new Cont(k => k(x));
var callCC = f => new Cont(k => f(x => new Cont(_ => k(x))).runCont(k));
var log = (x, ms) => new Cont(k => setTimeout(_ => k(console.log(x)), ms));
var omega = x => x(x); // This is a very dangerous function. Run `omega(omega)`.
callCC(omega).bind(cc => log("loop", 1000).bind(_ => cc(cc))).runCont(x => x);
function defclass(prototype) {
var constructor = prototype.constructor;
constructor.prototype = prototype;
return constructor;
}
This code is equivalent to:
forever:
delay(1000);
print("loop");
goto forever;
Hence, you should be careful when working with continuations.
[1] Continuations are usually implemented using first-class functions. However, in languages with first-class continuations like Scheme there's a distinction between continuations and functions. Nevertheless, even if a continuation is not a function it's still like a function in that it's invokable.