Thermometer continuations, part 1

Posted on February 12, 2020 by Calvin

Note: This blog post is still a rough draft. Read on with caution.

\[ \text{"Delimited continuations are the mother of all monads!"} \]

NB: I read this paper awhile ago, but I only recently decided to write a small post about it. You can find the code for this here.

The famous 1994 paper of Filinski showed denotationally that any monadic (computational) effect can be replicated via delimited continuations, e.g. exceptions/state/nondeterminism/etc. However, most languages do not expose continuations as a first-class citizen, as doing so usually involves capturing the function call stack as a value to be passed around in the code.

This post will be a summarized view of the functional pearl of Koppel et al. “Capturing the future by replaying the past”. There they show that in any language with exception and state, you can implement delimited continuations– the style they call as thermometer continuations. This is a cool idea, and allows us to implement multi-shot delimited continuations (i.e. can be used multiple times) in most languages. For reference, Python’s generators are considered one-shot delimited continuations, where the suspended generator after a yield statement is the continuation, but you can only run the continuation once when you decide to resume it.

Before we push onwards in the post, we just get an inkling of the general idea. We stated before that a delimited continuation is like an exception handler that can be resumed after execution– the try block acts as a shift operator where the continuation parameter is given by throw, while the except block is the part of the reset block outside of a shift which demarcates the continuation captured by the shift block. There’s really only one problem– for shift/reset, after applying the continuation, we can resume the computation in the shift block! Exceptions don’t normally do this.

The key intuition is this: just run it again! Actually, this isn’t even the intuition. It’s really the entire idea.

nondeterminism

A non-trivial effect that many people see is nondeterminism– if a function has many choices in its execution, then there may not be a single output to a given input. We would like to capture the outputs of all these choices in a composable fashion. The way we usually do this in functional languages is via a monad. For example, the function in Python

becomes monadic in Haskell:

But it would be nice if Python did have a primitive like choose so that we could write these functions without monads (which when unwinded come down to continuation-passing style). Let’s approach this one step at a time. Suppose we just want to perform nondeterminism with a single choice with only 2 options:

How can we get the list of possible outputs from each possible choice of this function? When the function hits the choose clause, there are two possible paths the function could go– 5 or 6. This has an obvious answer: run it twice, once with each of the choices. In code the choice operator becomes

Running the delimited continuation is given by effect handlers, which performs the key idea for thermometer continuations:

Running this on examples gives us great results!

Let’s extend this a bit to deal with choose clauses with more than two choices. Since a bool can only express 2 values, we need something else to represent the “state” of our continuation. Naturally we’ll store the index (and the total length of the choice list, since we need to know where to stop counting).

Aside from the slightly-added complexity of having an index as global state, the same idea stays the same: we replay the function repeatedly, each time using a new value of the choice function. Now the choose operator is a suitably extended version of the 2-choice version above:

where the effect handler with_nondeterminism is updated to run through the index state one-by-one, as opposed to just flipping a boolean state:

However, we notice that our global state can only deal with a single choose operator– how do we extend this to deal with multiple choose branches as in the original test_fn above? If you think about what the function execution would look like with all choices enumerated, this looks like a (stateful) tree. Performing a traversal of this tree while keeping state is effectively what an effect handler for choose would be doing! So taking a cue from iterative traversal techniques (see? coding interviews are helpful!), we will keep along our global state two stacks: the future and the past.

The past contains choices already made. The future contains the known choices to be made. The basic idea is that we record in our stacks the path taken by the execution of the program (this is similar to MCMC done in probabilistic programming languages). We then modify a single choice in our path at each new iteration, until we have exhausted all the possible paths possible through the tree.

How is this handler supposed to work? When the execution of the handler reaches a call to choose, it reads the choice from the future stack, and pushes the remainder to the past. If the future is unknown, then it means we have reached a choose statement for the first time, at which we pick the first choice and record it in the past stack.

Testing this gives us the expected behavior:

next steps

And that’s pretty much it. Delimited continuations are only a slightly more complicated version of this– we are traversing paths through execution trees as well, except this time the trees have state in their nodes. In the next post we’ll start looking at delimited continuations in the shift/reset paradigm and use this to build some cool algebraic effects in Python.