Skip to content

Continuation Passing Style

You're having a conversation with your buddy, who is a carpenter. (Yes, this story will have a point.) Your buddy has a beautiful piece of mahogany wood in his shed, and your plan today is to talk him into giving it to you so you can craft a table from it. In functional terms, we have

buddy :: Conversation -> Material
buddy conv | isConvincing conv = mahoganyWood
           | otherwise         = gnarlyBranch

Your buddy is a function that turns a conversation into a piece of mahogany wood or into a gnarly branch he found in his backyard, depending on your coercive skills to talk him out of that piece of mahoganyWood.

Actually, your buddy is a good guy who likes you and will only make you sweat for a while. In the end, he's going to give you the piece of mahogany wood no matter how ingenious your arguments that given you that piece of wood will make him happier:

buddy :: Conversation -> Material
buddy _ = mahoganyWood

Even better, your buddy is a really good friend. Given his training as a carpenter, he knows that he will do a much better job at whatever you want to do with this piece of wood than you would ever be able to do with your two left hands. So he tells you that he's not going to give you that piece of wood; he wants to know what you want to do with the wood, and he does it for you:

buddy :: Conversation -> (Material -> Product) -> Product
buddy _ theThingToMake = theThingToMake mahoganyWood

Given your description of theThingToMake from the piece of mahogany wood, a function of type Material -> Product, your buddy applies this function (rather expertly) to the piece of mahogany wood and gives you the beautiful table you wanted.

Ignoring the difference in carpentry skills between you and your buddy, the result is the same whether you provide your buddy with the procedure to turn the piece of wood into a table, your buddy applies this procedure to the piece of wood, and then gives you the table, or whether he gives you the piece of wood, and you apply the procedure to it yourself.

This is the essence of continuation passing style (CPS). A "normal" function returns some value of type a:

val :: a

In our first example above, buddy conv returned a value of type Material, the piece of wood. A function written in CPS does not return this value of type a directly. Instead, it wants to know what you want to do with this value and does it for you. The argument to such a function is a continuation of type a -> r that turns the value into a result of type r, and the function returns the result, of type r, that is obtained by applying this continuation to the value a.

Instead of

buddy :: Conversation -> Material

we have

buddy :: Conversation -> ((Material -> Product) -> Product)

More abstractly, instead of

f :: b -> a

we have

f :: b -> (forall r. (a -> r) -> r)

In the former case, f b returns a result of type a. In the latter case, it returns a result of type (forall r. (a -> r) -> r).

The forall r here is important, and it is allowed only after we turn on a language extension:

GHCi
>>> :set -XRankNTypes

forall r here says that we can turn a value of type a into a value of any type r, as long as we have a function of type a -> r.

It turns out that we can prove that values of type a are the "same thing" as values of type ((a -> r) -> r). Intuitively, this should make sense. Any value in our program is useful to us only if we do something with it. If we transform such a value x :: a using some function f :: a -> r, it makes no difference whether we apply f to x or whether we provide f to whichever function produced x, and we let that function apply f to x and then return the result of that application to us.

Formally, we say that the types a and (forall r. (a -> r) -> r) are isomorphic. This means that there exist mappings between values of these types that are inverses of each other. The round trip gives us the original value back. Here are these two mappings for the types a and (forall r. (a -> r) -> r).

toCPS :: a -> (forall r. (a -> r) -> r)
toCPS x = ($ x)

fromCPS :: (forall r. (a -> r) -> r) -> a
fromCPS x = x id

If you read the type signature of toCPS, it says that if we have a value of type a, we can turn this into a function which is able to produce a value of any type r, as long as you give it a function of type a -> r. The function that toCPS x produces is simply the function that applies its argument, a function, to x.

The type signature of fromCPS says that we can take any function that can turn a function of type a -> r into a value of type r and turn such a function into a value of type a. Indeed, we simply call our function x :: (a -> r) -> r with argument id. In this case, we have r = a, so x id must surely produce a value of type a.

We can try this out in GHCi:

GHCi
>>> :{
  | toCPS :: a -> (forall r. (a -> r) -> r)
  | toCPS x = ($ x)
  | fromCPS :: (forall r. (a -> r) -> r) -> a
  | fromCPS x = x id
  | :}
>>> fromCPS $ toCPS 5
5
>>> fromCPS $ toCPS "Hello world!"
"Hello world!"

Let's figure out why the roundtrip is the identity function:

fromCPS $ toCPS x = fromCPS ($ x)    -- Definition of toCPS
                  = ($ x) id         -- Definition of fromCPS
                  = id x             -- Definition of ($)
                  = x                -- Definition of id

So we can use functions of type ((a -> r) -> r) to represent values of type a.

Let's play with this idea using a really simple example, and then we return to what we really want to do: implement exception handling and coroutines.

First, let's implement a function to compute \(n!\). I know, can't we come up with a more interesting example already? It's a good example to illustrate what we want to do though. The "normal" way to do this is this:

fac :: Integer -> Integer
fac 0 = 1
fac n = n * fac (n - 1)

Never mind that this is not tail-recursive.

If we were consistent in using CPS, then we should convert all plain values of some type a in our program into functions of type forall r. (a -> r) -> r, but programming in CPS is not exactly natural. So we will do it only for values where we really need this strange encoding. In our implementation of fac, we don't "need" CPS anywhere, but let's pretend that we do care about using CPS for the return value, but not for the argument. Thus, we want a function

facCPS :: Integer -> (forall r. (Integer -> r) -> r)

If n = 0, then fac n = 1. facCPS does not return 1 directly. Instead it takes a continuation cont of type Integer -> r as argument and applies it to 1 for us:

facCPS 0 cont = cont 1

The implementation of facCPS n for n > 0 is more interesting. We could try

facCPS n cont = cont (n * fac (n - 1))

but this uses our "normal" function fac, which we want to replace with facCPS everywhere in our program. Instead of \((n - 1)!\), facCPS (n - 1) returns a function that allows us to apply some continuation of type Integer -> r to \((n - 1)!\). What exactly do we want to do with \((n - 1)!\) here? The goal of facCPS n is to apply cont to \(n!\). Thus, we need to multiply \((n - 1)!\) with \(n\) and then apply cont to it. That's what the continuation we pass to facCPS (n - 1) should do:

facCPS n cont = facCPS (n - 1) $ \f -> cont (n * f)

The whole implementation is

facCPS :: Integer -> (forall r. (Integer -> r) -> r)
facCPS 0 cont = cont 1
facCPS n cont = facCPS (n - 1) $ \f -> cont (n * f)

Have a closer look at what we did here. In our implementation of fac, we multiplied the return value of fac (n - 1) with n. Here, we passed an anonymous function to facCPS (n - 1), and facCPS (n - 1) calls this function with \((n - 1)!\) as its argument. In other words, the argument f of the function we pass to facCPS (n - 1) is exactly the same sa the return value of fac (n - 1). Within the function we pass to facCPS (n - 1), we do the same as before: we multiply \((n - 1)!\) with \(n\). Only now we don't want to return the result but invoke the continuation cont on this value. So that's what our anonymous function does: f -> cont (n * f).

Let's try it out:

GHCi
>>> :{
  | fac :: Integer -> Integer
  | fac 0 = 1
  | fac n = n * fac (n - 1)
  | :}
>>> :{
  | facCPS :: Integer -> (forall r. (Integer -> r) -> r)
  | facCPS 0 cont = cont 1
  | facCPS n cont = facCPS (n - 1) $ \f -> cont (n * f)
  | :}
>>> fac 10
3628800
>>> facCPS 10 id
3628800

What if we want to calulate \((n!)!\)? Using fac, this would be

GHCi
>>> fac (fac 4)
620448401733239439360000

Let's build the facCPS version up in steps. We want to call facCPS n. Whatever value this produces, we want to compute the factorial of:

GHCi
>>> facCPS 4 $ \f4 -> fac f4
620448401733239439360000

We observed above that fac n and facCPS n id compute the same thing. Thus, to obtain a version of this expression that uses only facCPS, we need to replace the call of fac f4 with a call to facCPS f4 id:

GHCi
>>> facCPS 4 $ \f4 -> facCPS f4 id
620448401733239439360000

This example is important for the following reason: In the expression fac (fac 4), the inner fac gets applied to 4, and then the outer fac gets applied to the result of fac 4. When using CPS, this is reversed. The outer facCPS produces the value \(4!\) and passes it to the anonymous function as its argument f4. The inner facCPS inside this function gets applied to f4. Since we pass the identity function id to facCPS f4, we obtain the value of f4!, that is, the value of \((4!)!\).

Note the game we are playing. In the continuation that we're passing to facCPS, the function parameter captures the value that the non-CPS version fac would have returned. This always works! Let's look at one last example, and then we get back to more meaningful uses of continuations.

Let's say we want to compute \(3! + 4! + 5!\):

GHCi
>>> fac 3 + fac 4 + fac 5
150

What does this look like in CPS? First, we call facCPS 3 and provide to it a function that captures \(3!\) as its argument f3:

>>> facCPS 3 (\f3 -> ...)

Remember, this function \f3 -> ... that we pass to facCPS 3 is a continuation. It's what we should do after facCPS 3 computes \(3!\). What is the next thing we should do? We should compute \(4!\). We do this by calling facCPS 4 with a continuation that captures the result in its argument f4:

>>> facCPS 3 (\f3 -> facCPS 4 (\f4 -> ...))

Now we still have to call facCPS 5 to compute \(5!\). The continuation we pass to facCPS 5 captures this value in its argument f5:

>>> facCPS 3 (\f3 -> facCPS 4 (\f4 -> facCPS 5 (\f5 -> ...)))

This last continuation that we pass to facCPS 5 should return the final result of our computation. That's not a problem now because, being nested inside the continuations that we passed to facCPS 3 and facCPS 4, it has access to all three parameters f3, f4, and f5. The result we want to return is f3 + f4 + f5:

GHCi
>>> facCPS 3 (\f3 -> facCPS 4 (\f4 -> facCPS 5 (\f5 -> f3 + f4 + f5)))
150