Continuation Passing Style

Table of Contents

A continuation takes the idea of a function’s return value and generalizes it. Normally a function returns a value to the expression that called the function in the first place. But what if a function could return somewhere else instead? We call this somewhere else a continuation.

1. Basic Continuations

1.1. Direct Style

The functions you have been writing until now have been in a form known as direct style. The functions take an argument, and then return a result to the calling expression.

Consider this simple example:

dec a = a - 1
double a = a * 2
inc a = a + 1
report a = a

We can run it like this:

Main> double (dec 3)
4

How does this happen? The first thing to execute is the call to dec. Function dec consumes the 3, and when it finishes, returns its result to the surrounding expression. The result is the expression double 2.

Let’s consider the role of double in this expression. It is waiting to receive the result of the dec 3 computation. We can represent this by writing “double \(\bullet\)”. This is an expression with a “hole” where the \(\bullet\) is. When dec returns, that hole is filled with the result, and the expression continues. We can call double the continuation of dec 3.

Consider another example:

Main> dec (double (inc 20))
41

The first function called is inc. When it finishes, it will return its result to the surrounding expression, which we represent as dec (double \(\bullet\) ) — its continuation. Function double consumes the result and returns 42 to its own continuation dec $\bullet$.

One way to think of a continuation is that it’s a way of identifying the rest of the program.

1.2. Continuation Passing Style

You will notice that the expressions we called continuations were like normal expressions, but each had a hole which we could fill with a value later. We have seen this before! Functions do the same thing: they are expressions with a hole (a named one, called a parameter) that we fill in later when we call the function.

The similarity is good enough that we can use functions to implement continuations. Consider the following program. Each of these functions takes an additional argument k as a continuation, and the continuation represents “what comes next” in the program.

cdec a k = k (a - 1)
cdouble a k = k (a * 2)
cinc a k = k (a + 1)
report a = a

Let’s try this out:

Main> cdec 3 report
2

Here, cdec took an argument 3 and a continuation report. When it had finished performing its computation with 3, it passed the result to report rather than returning the result to the calling expression. This is one of the properties of functions written in CPS: they do not return Truth in advertising: in Haskell, the report function actually does return the final result. But you will notice that it is a tail call, so the computation is over by the time that happens. In a language that supports real continuations, a “final” continuation such as report would not return at all. . Instead, they call their continuation directly.

Another way to look at this is to say that a continuation is a generalization of a return keyword. We can override what return does. Instead of returning to the expression which called it, a function can pass its result to an arbitrary location. (Imagine what our code would look like if we used the name return (which is not a keyword in Haskell) instead of k.)

We can rewrite the double (dec 3) example in CPS like this:

Main> cdec 3 (\v -> cdouble v report)
4

Here, cdec took 3 as an argument and (\r -> cdouble r report) as a continuation. When cdec finished, it passed its result to the continuation instead of returning it. The resulting expression was cdouble 2 report. Next, double consumed the 2 and passed its result to the continuation report.

Our second example looks like this:

Main> cinc 20 (\v1 -> cdouble r1 (\v2 -> dec v2 report))
41

It is only slightly more involved than the first example.

Note that we can rewrite the example this way:

Main> cinc 20 (\v1 ->
  cdouble v1 (\v2 ->
  cdec v2 report))
41

You can pretend this is an imperative language. The first line “assigns” 20 to v1. The second line “assigns” cdouble v1 to v2. The third line “decrements” v2 and reports it.

Let’s look at a more complicated example.

cinc a k = k (a+1)
cdec a k = k (a-1)
cadd a b k = k (a+b)
report a = a

Suppose you wanted to evaluate the expression add (inc 4) (dec 5). How would you do it using the continuation passing style versions above?

Pretending for a moment that Haskell isn’t lazy, the first thing to execute is inc 4, so we would need call cinc first and save its result in the continuation. Next, we would want to call cdec, and again save its result. Finally, with the two saved results, we can call cadd, and pass the final answer to report. Here is what it looks like:

Main> cinc 4 (\a ->
  cdec 5 (\b ->
  cadd a b report))
9

Please make sure you understand how that code works. Notice that the first continuation encloses the second one and that the inner continuation (the \b -> ... one) has access to the scope of the outer one.

But now consider this code:

Main> cdec 5 (\b ->
      cinc 4 (\a ->
      cadd a b report))
9

It is different from the previous example in only one way: the order of operations has changed. In the direct style example, the compiler has the option (in some languages, such as C) of evaluating dec and inc in any order. But, if we use CPS, then there is only one order of operation for the expression. What CPS has done for us is it has exposed the program’s flow of control, and allowed us to constrain it.

In review, this is what we have so far:

  • A function written in CPS doesn’t return. Instead it passes its result to another function.
  • A continuation represents “the rest of the computation”.
  • The order of operations in CPS code is explicit and constrained.
  • Calls to continuations are always tail-calls.

2. Continuations and Recursion

Continuation passing style can do some interesting things in recursive functions.

Consider the multlist function below. It simply multiplies a list of integers together.

multlist [] = 1
multlist (x:xs) = x * multlist xs

If we were to run it with the input [2, 3, 4] we would get the following execution trace. Each line represents the next step of the program.

1: multlist [2, 3, 4]
2: 2 * (multlist [3, 4])
3: 2 * (3 * (multlist [4]))
4: 2 * (3 * (4 * (multlist [])))
5: 2 * (3 * (4 * 1))
6: 2 * (3 * 4)
7: 2 * 12
8: 24

As the computation progresses, we stack up recursive calls until we reach the base case, and then return from all the recursive calls to compute the result.

Consider what’s happening in line 2 of the sample run: “2 * \(\bullet\)” is the continuation for the expression (multlist [3, 4]).

2.1. Accumulator Recursion

Another style of recursion avoids the use of the stack by accumulating the value in a separate parameter. The amlutlist function below is an example:

amlutlist []      acc = acc
amlutlist (x::xs) acc = amultlist xs (x * acc)

If we were to run it with the input [2, 3, 4] and 1 we would get

1: amultlist [2, 3, 4] 1
2: amultlist [3, 4] (* 2 1)
3: amultlist [3, 4] 2
4: amultlist [4] (* 3 2)
5: amultlist [4] 6
6: amultlist [] (* 4 6)
7: amultlist [] 24
8: 24

Notice this time that we only need to keep track of a single call to multlist at any one time… the result of one call is simply the value of the next call. We discussed this in the chapter on tail-recursion. Tail recursive functions allow the compiler to eliminate the call-stack, speeding things up and reducing the amount of memory we need on the stack.

Notice the flow of the computations. In the simple recursion example the function first descended to the end of the list and then performed the computations as the recursive calls returned. It the tail recursive example the computations occur first, and then the function passes the result to the recursive call.

2.2. Continuation Passing

Integers and lists are not the only things a function can accumulate. We can also accumulate computations. Consider kmultlist, which is the same as amultlist but it is in CPS.

kmultlist []     k = k 1
kmultlist (x:xs) k = kmultlist xs (\v -> k (x * v))

As before, we have an extra argument k to specify where the result should go. If we call kmultlist on the empty list, it will pass 1 to k.

The recursive case is more complex. Suppose we call kmultlist on a non-empty list x:xs. In order to compute the result, kmultlist needs to make a recursive call on xs.

This function needs a continuation to tell it what to do with that result. This continuation captures the result of the recursive call, multiplies it by x, and then passes that to the continuation given to the initial call.

You should notice a correspondence between the continuation and the original direct-style version:

multlist (x:xs) = x * multlist xs

The v parameter is equivalent to multlist xs, and the call to k represents returning the value to the caller.

Here is a sample run.

 9: kmultlist [2,3,4] report
10: kmultlist [3,4] (\v1 -> report (2 * v1))
11: kmultlist [4] (\v2 -> (\v1 -> report (2 * v1)) (3 * v2))
12: kmultlist [] (\v3 -> (\v2 -> (\v1 -> report (2 * v1)) (3 * v2)) (4 * v3))
13: (\v3 -> (\v2 -> (\v1 -> report (2 * v1)) (3 * v2)) (4 * v3)) 1
14: (\v2 -> (\v1 -> report (2 * v1)) (3 * v2)) (4 * 1))
15: (\v1 -> report (2 * v1)) (3 * 4))
16: report (2 * 12)
17: report 24

Each recursive call builds a new, larger continuation out of the one it received.

2.2.1. Comparing

In these three coding styles there are two parts to the problem. To multiply together the elements of a list, you first multiply the elements of the rest (i.e., the tail) of the list, and then multiply the first element to that result.

  • In direct style, the function recurses until it reaches the end of the list and performs the multiplications “on the way out”.
  • In accumulator recursion, the function performs the multiplications “on the way in” using an accumulator.
  • In CPS, the recursive call builds a function which, when called with the base case, will perform the computation.

3. Multiple Continuations

As we said, a continuation is like a return address, made explicit. Because we store continuations in variables, there is no reason we can’t have more than one of them. Here is a new version of kmultlist function that can “bail out” of a computation. It passes its continuation to a helper function aux. Within aux there are two variables now that have continuations in them. It accumulates ks (the “succeeding” continuation) but also can call the original continuation k (the “abort” continuation). The continuation sr represents the function’s computation; call it, and the computation occurs. The k continuation, if called, will skip all that computation and (appear to!) exit out of all the levels of recursion, back to the beginning.

kmultlist xx k = aux xx k
  where aux []     ks = ks 1
        aux (0:xs) ks = k 0
        aux (x:xs) ks = aux xs (\v -> ks (x * v))

Here is a sample run where we see the abort continuation in action:

18: kmultlist [2, 3, 0] report
19: aux [2, 3, 0] report
20: aux [3, 0] (\v1 -> k (2 * v1))
21: aux [0] (\v2 -> (\v1 -> k (2 * v1)) (3 * v2))
22: report 0
23: 0

For that matter, why stop at two continuations? This next version interprets negative numbers in a strange way: if it finds a number \(-n\), it undoes n levels of recursion, and activates the computation at that point.

nth 0 (x:xs) = x
nth n (x:xs) = nth (n-1) xs

kmultlist xx k = aux xx [k]
  where aux []     klist = (head klist) 1
        aux (0:xs) _     = k 0
        aux (x:xs) klist | x < 0 = (nth (0-x) klist) 1
                         | otherwise = aux xs ( (\v -> (head klist) (v * x)) :: klist)

The following sample run may illustrate the operation of the new function. Be sure you understand how it works.

Main> kmultlist [2, 3, 4, 5, 6, 1] report
720
Main> kmultlist [2, 3, 4, 5, 6, 1, 0, 2, 4, 6] report
0
Main> kmultlist [2, 3, 4, 5, 6, -1, 0, 2, 4, 6] report
120
Main> kmultlist [2, 3, 4, 5, 6, -2, 0, 2, 4, 6] report
24

Continuations, then, are a means of time travel. A continuation allows us to save a point of the program and return to it any time we want.

Author: Mattox Beckman

Created: 2022-02-19 Sat 16:02