Thoughts on Continuations
Disclaimer: I don’t have a hard point here, only thoughts. Hence the title.
A hot topic in programming languages these days is continuations/callbacks. Javascript is perhaps the language which has brought continuations to the mainstream along with all the headaches they can cause in an async world. But, as we’ve seen in Python, Rust, C#, Haskell, OCaml, this is an issue that gets addressed everywhere.
Monads, disguised under a different name (and without the types), came in to bring sanity back to nested callback-hell. People seem to enjoy chaining .then
calls in JS/Rust because it can be nicer to read than heavily indented nested functions.
Taking it another step further, languages can provide support to make those functions “disappear”. Examples:
- Haskell - `do`
- Python 3.4 & ES6 - generators
- Python 3.5+ & ES7 - async def (generators with restrictions)
- OCaml - lwt macros (not up to date on this)
- C# - compiler makes state machine
- Clojure core.async - macros make state machine
The goal of these mechanisms is to remove the explicit creation of a continuation and instead put the burden on the compiler/whatever. Why? Because code reads more like a boring old set of assignments. It turns: foo(10, λ x: bar(x))
into let x = foo(10); bar(x)
. This is most obvious to me when thinking about how Python & JS hijack generators to do this; generators suspend a function (creating a continuation) with some value that needs to be fulfilled and then the generator can be resumed when its fulfilled.
The drawback to these approaches is the same as its advantages. While it may look like normal code again, it really is still restricted and calling await
(or the like) has to happen in an async
function (or the like) and so on up the chain. And this shouldn’t be too surprising because the whole reason we’re using continuations in the first place is to adhere to a “restricted permission” execution.
Why are we using continuations?
Let’s take a step back from the async world and look at continuations elsewhere.
Iterators
If you’re in a language with opaque types (to hide the implementation), then you might want to use a callback to give your users a way to iterate over the structure. Since they don’t know the internal structure (and you don’t want them to), you restrict their access by accepting a callback which you call on every element.
Case analysis
This one only applies to languages with static typing. The following uses a continuation (if you squint hard) as the enforcement that i
is actually an int, and is not bound in the None
arm.
match maybe_an_int with
| Some i -> i + 1
| None -> 0
Async
And we’re back. Async needs continuations to let multiple computations, which may have to wait an arbitrary time until they can proceed, interleave with each other to provide concurrency. Continuations let these computations pause and be resumed by a scheduler.
One of these isn’t like the other
I haven’t nailed down exactly why, but async seems like a different use-case for continuations than something like iteration. To explain why, I must take a slight detour on “alternatives” to continuations
Threads are continuations
All that talk about suspending, scheduling, and resuming a computation sounds an awful lot like an OS and processes/thread (memory access aside) management. The OS can do this without any knowledge of the language, runtime, etc. you’re using because it can a) interrupt your computation whenever it wants and b) maintain the whole state of the computation. There are pros/cons of using threads as the solution for async operations, but just remember that a language with async stuff is acting like a mini-kernel all over.