slightly more advanced continuations (with a more contrived example)

03/18/2024
Go back

In the previous article, we spoke about what a continuation is and what it does. You should read that if you haven't before continuing on.

Now, how is it doing it? Well, there are definitely a few potential ways.

A continuation must store an ip/ep pointer, so in itself, it can be a very compact data structure, but keeping that "environment" around is a different story. A very straightforward, orthogonal implementation might copy the entire contents of the call stack into the heap, point the ep at that, and say "there you go". By the magic of garbage collection, as long as you keep your continuation around, that object (your call stack) and its subsidiaries (and possibly registers -- though let's not worry about that), won't go away for as long as your continuation lives.

One could imagine a more complicated schema, in which the compiler statically analyzes your program and suspiciously must place activation records on the heap based on who it thinks might use a continuation, even in one of its subsidiary functions, but that is likely not how it is done.

Now let's look at a cool example: what does this do: (call/cc call/cc). Any thoughts?

Personally, I couldn't do this mid-conversation with somebody when brought up in my head. After the call was over, I went and wrote down on a piece of paper and figured it out, so here it is:

Let's disect it: (call/cc call/cc). Recall that the outer call/cc is being called and it's giving its continuation as a parameter into the inner call/cc and in a "normal" way hoping to return whatever value that gives back, which may include the continuation, to further be used later. So the inner call/cc receives the outer continuation, let's call it outerK. Now what does this do? Well, it's being called, and it has a parameter, so it does the same thing. It creates its own continuation, let's call it innerK, and it passes it as an argument to the function that's given as its parameter and returns whatever it returns. So at this point, we have that the inner call/cc calls the following: (outerK innerK). Well, we've hit the bottom. outerK is a continuation, and thus "continues execution" as if the outer call/cc had returned the value given as its parameter, so we get back the inner continuation.

What does this mean for us? Well, you can use innerK now. Anytime you do, it's as if the inner call/cc returned the value given as the parameter to innerK. The inner call/cc was called by the outer call/cc with the hopes of returning whatever it returns, so it's as if the outer call/cc returned that value. In essence, it then acts as a regular continuation for the outer call/cc.

What have we achieved? A continuation, though with a lot of extra work. This functions entirely to the naive user as if you simply called (call/cc (lambda (k) k)) and got back your continuation.

To the more sophisticated user, they may recognize that in general, call/cc has one "normal" return (the first one, assuming the continuation was not invoked in the FUNC it was passed) and many "abnormal" (via invoking the continuation). In this case, it is the opposite. There are many "normal" (since the FUNC may return multiple times -- via invokation of its continuation) but one "abnormal" (since FUNC invokes the outer call/cc's continuation inside it, and throws it away).