"You are responsible for explicitly understanding the rest of the program." A sentence like this appears in every tutorial explaining first-class continuations, a programming language construct that appears in Lisp, Scheme, and Haskell, and if you look at people discussing them they find such constructs "scary."

One of the things I did many, many moons ago was write a couple of small Lisp interpreters. (It was long enough ago that I'm sorry to say they were written in CoffeeScript.) One thing that I did finally understand while building these was how continuations could be used to replace almost all flow control, that if, while, and even function calls were all just specialized versions of continuations.

The problem with continuations is that they do look scary. But while watching Alexis King's awesome presentation Demystifying Delimited Continuations, I'm glad to say that I finally wrapped my head around the notation for continuations in a way that clarifies that terrifying sentence you find in every explanation of continuations, "A continuation is the code you write that will handle the rest of the program."

In Alexis's example, she writes about try/catch, and she describes it this way. When you call a function, it is run in an "evaluation context" which includes all free variables, those defined globally or within in a parent scope, which is absolutely necessary because some of those variables are function names you want to call. Let's call this evaluation context 𝐄. It contains everything outside the current function which that function needs to evaluate correctly. Let's annotate this function call this way: 𝐄[f()].

Alexis describes try/catch in the scenario where f1 throws an exception, using this notation where the try part defines an inner continuation and catch defines what to do with the value that inner continuation generates:

  • 𝐄[catch{𝐄₂[f1()], f2}]
  • 𝐄[catch{𝐄₂[throws(v)], f2}]
  • 𝐄[f2(v)]

As far as the function f1() in concerned, the continuation 𝐄₂ is "the rest of the program," but notice what the continuation really is: it's just an purely boolean if statement. It says that if the return value of the left is as exceptional value, execute the function on the right with that value within the context of some parent execution context 𝐄.

Try/Catch vs Result

In Rust, 𝐄₂ would be function that calls a function with a Result<Ok, Error> return value:

pub fn sqrt(x: f64) -> Result<f64, MathError> {
        if x < 0.0 {
            Err(MathError::NegativeSquareRoot)
        } else {
            Ok(x.sqrt())
        }
    }

The receiving function would be responsible for choosing what to do with each value.

... and that's it. That's all they are. This is why I like Rust a lot; it makes explicit all that painful "stack rewinding" which was nothing more than a convenience method for letting people out of deep libraries that were failing. Rust has taught us all that "deep libraries" were, by themselves, a mistake. The Linux world used to say "With enough eyes, all bugs are shallow," but in this era of explosive growth and constant innovation, there just aren't enough eyes trained well enough to evaluate. Each library should have a facade, the API boundary layer, that it exposes to the world, but each library should also, on its own, be no deeper than it absolutely must be to maintain a coherent story in the face of failure.

Looking through the effort Alexis went through to get continuations added to Haskell, there's obviously a lot more to it than that. In our toy example, try creates a place on the stack where we can "rewind" to in an emergency, but the whole rest of the stack is represented by 𝐄. How you want to preserve that stack, how large you want that preserved stack to be, when you give a continuation a name and allow it to be passed out of the current execution frame, is what makes "first class" continuations so difficult for people to understand.

You're probably familiar with Jason Heeris's comic, Why You Should Never Interrupt Your Programmer, because he's busy holding a huge amount of context in his head, simulating as much of the program as he needs to, to get the job done. I've found that making the job as small as possible is how you avoid this problem, by being explicit about your levels of abstraction, and the levels of responsibility in code that you're currently working on. By avoiding having to know more than "what's going on one level up," you keep yourself sane and productive. Understanding continuations helps by making explicit what that one level up is, even if you don't actually use continuation-specific syntax or semantics in your language of choice.


Postscript: I just want to add that if you're using a language without stack frames, or a language with [dynamic scoping] (Emacs Lisp, Bash, PowerShell), you're kinda on your own. Those are a whole different beast. Emacs Lisp, at least, recognized just how maddening dynamic binding is, and made lexical binding an "option" in the same sense than use strict is an "option" in JavaScript: sure it's "optional," in that everything written after its introduction must use it or your linter will be very cross with you.