4

I try to implement the continuation monad in Javascript to handle continuation passing style and asynchronous control flows. Here is my continuation monad for learning:

// auxiliary functions

const log = prefix => x => console.log(prefix, x);
const addk = x => y => k => setTimeout((x, y) => k(x + y), 0, x, y);
const inck = x => k => setTimeout(x => k(x + 1), 0, x);
const sqr = x => x * x;


// continuation monad

const cont = {
  of: x => k => k(x),

  map: ftor => f => k => ftor(x => k(f(x))),

  ap: ftor => gtor => k => ftor(x => gtor(f => k(f(x)))),

  chain: ftor => mf => k => ftor(x => mf(x) (k)),

  callcc: f => k => f(x => () => k(x)) (k)
};


// map a normal, unary function with an asynchronous function:

cont.map(inck(9)) (sqr) (log("map"));


// chain two asynchronous CPS computations with an asynchronous binary CPS function

const comp1 = cont.map(inck(4)) (sqr);
const comp2 = cont.map(inck(9)) (sqr);

cont.chain(comp1) (x => cont.chain(comp2) (y => addk(x) (y))) (log("chain"));

Apart from cont.ap, whose benefit doesn't reveal itself to me, everything works fine.

Now I'd like to mimic the throw/catch mechanism of synchronous control flows in Javascript. callcc seems to fit, because it provides an escape continuation mechanism for use with continuation monads, as stated on http://hackage.haskell.org/.

But I can't figure out how to apply callcc and I haven't found any suitable source that describes such an application.

  • Just as an aside, highly recommend using something like `R.curryN` over manually currying all your functions http://ramdajs.com/docs/#curryN – Jared Smith Sep 30 '16 at 12:57
  • @JaredSmith there is no Rambda tag on this question. Also, what is the basis of your recommendation? – Mulan Sep 30 '16 at 15:58
  • @Jared I consider it the other way around: Why should I use a programmatic curry solution when I have arrow functions with little syntactic overhead in my tool set? –  Sep 30 '16 at 16:01
  • @naomik could just as easily have said `_.curry`, I just prefer ramda. As for the why, I personally prefer the flexibility of being able to pass any number of arguments or one at a time. – Jared Smith Sep 30 '16 at 16:01
  • 2
    @ftor, James Long does a good series on this: ["What is a continuation?"](http://jlongster.com/Whats-in-a-Continuation), ["Implementing a stepping debugger in JavaScript"](http://jlongster.com/Implementing-Stepping-Debugger-JavaScript), and ["Exploring Continuations: Resumable Exceptions"](http://jlongster.com/Exploring-Continuations-Resumable-Exceptions). He uses a utility he wrote, called [Unwinder](https://github.com/jlongster/unwinder), which is mostly just a exploration/demonstration of how call/cc can be implemented in JS. Disclaimer: it's all very highly involved. – Mulan Sep 30 '16 at 16:03
  • @JaredSmith there is no lodash or underscore tag on this question. – Mulan Sep 30 '16 at 16:04
  • @ftor YMMV, after all its not that hard to write your own implementation if that's the way you swing. For trivial examples or your own code it almost certainly doesn't matter, but if your exporting an API for the benefit of others saying something like `f(3)('foo')(true)` is less idiomatic for JS than `f(3, 'foo', true)`. – Jared Smith Sep 30 '16 at 16:04
  • @naomik: Thanks for the unwinder link. Looks promising! –  Sep 30 '16 at 16:06
  • @JaredSmith I'm positive you are trying to be helpful but your comments are off-topic – this isn't a question about how/why to curry (or not to curry) functions. Also, your personal preference doesn't make a very good supporting argument. – Mulan Sep 30 '16 at 16:07
  • @ftor you're going to be a little disappointed, I'm sure. Unwinder involves a "pre-compile" step, of sorts. And the author recommends against using it on production code. I think there are just some features that can't be implemented without support from the language itself. – Mulan Sep 30 '16 at 16:09
  • @Jared I think mathematical curried functions are less idiomatic for ES5 but not for ES6 (=> arrow functions). But it is OK if we disagree on this issue. –  Sep 30 '16 at 16:11
  • @ftor for writing reading code, totally agree with you. I was more considering it from the caller's perspective. If the caller is you, no problems. – Jared Smith Sep 30 '16 at 16:49

1 Answers1

7

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.

Community
  • 1
  • 1
Aadit M Shah
  • 72,912
  • 30
  • 168
  • 299
  • 1
    You can delete, edit, and later undelete your answer if you accidentally posted it prematurely. – Bergi Oct 03 '16 at 17:50
  • I can't wait to read the rest of your answer, surely you'll not only hit on invokable functions as dangerous but also on continuations being equivalent to GOTO? – Bergi Oct 03 '16 at 17:51
  • Thank you for taking the time, @Aadit. I like your approach to explain `call/cc` by using examples in Scheme, Haskell and Javascript. I guess this Q&A is a good starting point for people, who want to play around with `cont`/`call/cc` in Javascript and if this leads to a few crashed browsers due to infinite `goto` loops - well, I am OK with that. Anyway, now that I have a better understanding of `call/cc` I wonder what happens if I pass `cont` to `EitherT`. Keeping `call/cc` in mind this tool set seems to be promising to handle asynchronous control flows in a lazy, unicast and monadic style. –  Oct 04 '16 at 06:22
  • @Aadit Why do you wrap `Cont` in an object? In other words, why `Cont.of = x => new Cont(k => k(x))` instead of `Cont.of = x => k => k(x)`? Is a continuation in Haskell wrapped too? Or do you just want to create a "continuation type" and the only way to create a type in Javascript consists in assigning methods to a prototype, so you need an object that references this prototype? –  Oct 21 '16 at 10:52
  • @ftor It's just so that we can use `map`, `apply` and `bind` as methods instead of functions. It's also so that we can implement `map`, `apply` and `bind` as generic functions. For example, you can implement a generic map as `var map = f => x => x.map(f);`. This `map` function is allows for partial application and works for any object which implements the `map` method. Hence, you can use it with continuations as well as arrays and any other type. – Aadit M Shah Oct 22 '16 at 04:56
  • It is worth noting that you always need an auxiliary function like `run` when you deal with monads that contain functions as values, because the returned lambda is wrapped in a monad instance (an `Object` type in Javascript). It is the same with the `State` monad for instance. –  Nov 17 '16 at 10:11