Continuation-passing style
Continuation-passing style (CPS) is a way of writing programs in which no function returns. Each function takes an extra argument, its continuation, a function that represents the rest of the computation, and passes its result to that continuation instead of returning it. In CPS every call is a tail call, the order of evaluation is written out explicitly, and control operators such as exceptions, backtracking and call/cc become ordinary functions. Compilers for functional languages use CPS, or the closely related A-normal form, as an intermediate language[8].
Overview
In , once has been computed, what remains is to multiply it by 2 and add 1. That remaining work, as a function of the value of , is the continuation of the call: . Every expression in a program has a continuation, and in direct style it is implicit: the language's runtime keeps it on the call stack, as the frames waiting for results. CPS passes it explicitly as an argument.
Factorial and Fibonacci in direct style and in CPS.
(* Direct style: the multiplication waits for the recursive call. *)let rec fact n = if n = 0 then 1 else n * fact (n - 1)(* CPS: the waiting work moves into the continuation k, and every callis a tail call. *)let rec fact_k n k = if n = 0 then k 1 else fact_k (n - 1) (fun r -> k (n * r))(* Fibonacci has two recursive calls; CPS fixes their order. *)let rec fib_k n k =if n < 2 then k n else fib_k (n - 1) (fun a -> fib_k (n - 2) (fun b -> k (a + b)))
Running it.
fact 10 -> 3628800fact_k 10 (fun x -> x) -> 3628800fact_k 5 string_of_int -> "120"fib_k 20 (fun x -> x) -> 6765
In fact_k the multiplication that direct-style fact performs after its recursive call is moved into the continuation fun r -> k (n * r), and the recursive call is the last thing the function does. The final continuation decides what happens to the answer: the identity returns it, string_of_int converts it. In fib_k the two recursive calls, which direct style would leave in an unspecified order, are sequenced: the second starts inside the continuation of the first.
Definition
A term is in CPS when every function takes a continuation parameter, every call is in tail position, and every intermediate result is named. The types change systematically. For a fixed answer type , a computation producing an becomes a function that takes an -consumer:
so a function of type int -> int, like fact, becomes int -> (int -> 'r) -> 'r, like fact_k.
The CPS transform
Any program can be converted to CPS mechanically. Plotkin's call-by-value transform[2] is defined by induction on terms, where are fresh variables:
Variables and λ-abstractions are values, and a value is passed straight to the continuation. An application evaluates the function, then the argument, then calls the function with the argument and the application's own continuation. The transformed program is run by applying it to the identity continuation. Plotkin proved that the transform is correct: evaluates to the translation of the value of , and it does so under call-by-name and call-by-value alike, because every argument in a CPS term is already a value[2].
The transform for a small language with addition, and an evaluator to check it.
(* A small call-by-value language and Plotkin's CPS transform of it. *)type term =| Const of int| Var of string| Lam of string * term| App of term * term| Add of term * termlet counter = ref 0let fresh base = incr counter; Printf.sprintf "%s%d" base !counter(* [x] = \k. k x [\x.e] = \k. k (\x. [e])[e1 e2] = \k. [e1] (\f. [e2] (\v. f v k))[e1 + e2] = \k. [e1] (\a. [e2] (\b. k (a + b))) *)let rec cps = function| Const n -> let k = fresh "k" in Lam (k, App (Var k, Const n))| Var x -> let k = fresh "k" in Lam (k, App (Var k, Var x))| Lam (x, e) -> let k = fresh "k" in Lam (k, App (Var k, Lam (x, cps e)))| App (e1, e2) ->let k = fresh "k" and f = fresh "f" and v = fresh "v" inLam (k, App (cps e1, Lam (f, App (cps e2, Lam (v, App (App (Var f, Var v), Var k))))))| Add (e1, e2) ->let k = fresh "k" and a = fresh "a" and b = fresh "b" inLam (k, App (cps e1, Lam (a, App (cps e2, Lam (b, App (Var k, Add (Var a, Var b)))))))(* A call-by-value evaluator for both the source and the transformed term. *)type value = Int of int | Clo of string * term * (string * value) listlet rec eval env = function| Const n -> Int n| Var x -> List.assoc x env| Lam (x, e) -> Clo (x, e, env)| App (e1, e2) -> (match eval env e1 with| Clo (x, body, cenv) -> eval ((x, eval env e2) :: cenv) body| Int _ -> failwith "not a function")| Add (e1, e2) -> (match (eval env e1, eval env e2) with Int a, Int b -> Int (a + b) | _ -> failwith "not a number")let rec show = function| Const n -> string_of_int n| Var x -> x| Lam (x, e) -> "\\" ^ x ^ ". " ^ show e| App (e1, e2) -> "(" ^ show e1 ^ ") (" ^ show e2 ^ ")"| Add (a, b) -> show a ^ " + " ^ show blet rec size = function| Const _ | Var _ -> 1| Lam (_, e) -> 1 + size e| App (a, b) | Add (a, b) -> 1 + size a + size b
Running it.
program (\x. x + 1) (41)direct result 42cps program, run with the identity continuation: 42term size 6 before, 36 after the transformcps of 41 \k11. (k11) (41)
Administrative redexes
The transform above is correct but wasteful: the term grows from 6 nodes to 36, most of them λs that are immediately applied, such as the produced for every variable in function position. These administrative redexes can be reduced at transformation time. Danvy and Filinski gave a one-pass transform that uses functions of the metalanguage for the continuations known at transformation time and so produces no administrative redexes at all[9]. Flanagan, Sabry, Duba and Felleisen observed that a compiler which transforms to CPS, optimizes and transforms back computes the same thing as one that only names every intermediate result, which they called A-normal form[10]:
Properties
Every call is a tail call
No function in CPS has anything left to do after a call, so every call can be compiled as a jump and the stack does not grow. The work that direct style keeps in stack frames is in the continuation closures instead, which live on the heap. A recursive function that would exhaust the stack therefore runs in CPS, at the cost of one allocation per pending frame:
Summing a tree of depth one million in direct style, in CPS, and with the continuation defunctionalized.
type tree = Leaf | Node of tree * int * tree(* Direct style: one stack frame per level of the tree. *)let rec sum = function Leaf -> 0 | Node (l, x, r) -> sum l + x + sum r(* CPS: every call is a tail call; the pending work lives in closureson the heap. *)let rec sum_k t k =match t with| Leaf -> k 0| Node (l, x, r) -> sum_k l (fun a -> sum_k r (fun b -> k (a + x + b)))(* Defunctionalizing the continuation: one constructor per lambda above,and an [apply] that does what the lambda did. The result is a loopover an explicit stack. *)type cont = Done | Right of tree * int * cont | Combine of int * int * contlet rec sum_d t k =match t with Leaf -> apply k 0 | Node (l, x, r) -> sum_d l (Right (r, x, k))and apply k v =match k with| Done -> v| Right (r, x, k) -> sum_d r (Combine (v, x, k))| Combine (a, x, k) -> apply k (a + x + v)(* A left spine of n nodes: depth n. *)let spine n =let rec go i acc = if i > n then acc else go (i + 1) (Node (acc, i, Leaf)) ingo 1 Leaf
Running it with an 8 MB system stack.
sum (depth 1,000,000) -> Stack_overflowsum_k (depth 1,000,000) -> 500000500000sum_d (depth 1,000,000) -> 500000500000
The direct version needs one frame per level and fails once the depth exceeds the stack limit (OCaml 5 runs OCaml code on its own stacks, limited by the l parameter of OCAMLRUNPARAM instead of the system stack). See tail call.
Defunctionalized continuations
The continuations of a particular CPS program are created by a fixed set of λs, so they can be replaced by a data type with one constructor per λ, holding its free variables, and an apply function, as in sum_d above[12][13]. The continuation data type is then exactly an explicit stack, and the program a loop that pushes and pops it. Applied to an interpreter, CPS followed by defunctionalization turns a recursive evaluator into an abstract machine, which is how Ager, Biernacki, Danvy and Midtgaard derived the CEK and Krivine machines from evaluators[17].
Evaluation order
Direct style leaves some evaluation order to the language: OCaml evaluates the operands of a + b in an unspecified order (in practice right to left). A CPS term fixes it, since the only way to get a value is to be passed it by a continuation, and a continuation is called by one computation after it has finished. This is why the result of Plotkin's transform is independent of the evaluation strategy used to run it, and why a CPS transform for call-by-name differs from the call-by-value one: it decides when arguments are evaluated.
Control operators
Because the continuation is a value, a function in CPS can choose not to call it, call a different one, or call it more than once. Control flow that direct style needs special constructs for is expressed by these choices.
Exceptions and backtracking
With two continuations, one for success and one for failure, a function reports an error by calling the failure continuation, which skips the rest of the success path. When the failure continuation means "try the next alternative", the same pattern is backtracking search:
Exceptions and backtracking with success and failure continuations.
(* Two continuations: one for success, one for failure. Exceptions withoutan exception mechanism. *)let safe_div a b ~ok ~err = if b = 0 then err "division by zero" else ok (a / b)let calc a b c ~ok ~err =safe_div a b ~err ~ok:(fun x -> safe_div x c ~err ~ok:(fun y -> ok (x + y)))(* Backtracking: the failure continuation says what to try next. Find asubset of the list summing to a target. *)let rec subset_sum xs target ~found ~fail =if target = 0 then found []elsematch xs with| [] -> fail ()| x :: rest ->subset_sum rest (target - x)~found:(fun s -> found (x :: s))~fail:(fun () -> subset_sum rest target ~found ~fail)(* Counting every solution: on success, record it and ask for the next. *)let count_solutions xs target =let n = ref 0 inlet rec go xs target ~found ~fail =if target = 0 then found ()else match xs with| [] -> fail ()| x :: rest -> go rest (target - x) ~found ~fail:(fun () -> go rest target ~found ~fail)ingo xs target ~found:(fun () -> incr n) ~fail:(fun () -> ());!n
Running it.
calc 100 5 2 -> ok 30calc 100 0 2 -> error: division by zerosubset_sum [8; 6; 7; 5; 3] 16 -> [8; 5; 3]
In subset_sum each choice to include an element passes on a failure continuation that retries without it, so when a branch fails the search resumes at the most recent choice. This is the standard implementation of Prolog-style backtracking and of the backtracking alternative in parser combinators.
call/cc
Scheme's call-with-current-continuation gives a direct-style program access to its own continuation. callcc f calls f with a function that, when called with , abandons whatever it is doing and makes callcc f return . In an interpreter written in CPS it is a single case, because the interpreter already holds the continuation:
A CPS interpreter with call/cc.
(* An interpreter written in CPS. Because the interpreter holds thecontinuation as a value, it can hand it to the program: call/cc. *)type expr =| Num of int| Var of string| Lam of string * expr| App of expr * expr| Add of expr * expr| Mul of expr * expr| Callcc of expr (* callcc (fun k -> ...) *)type value = Int of int | Fn of (value -> (value -> value) -> value)let rec eval env e (k : value -> value) : value =match e with| Num n -> k (Int n)| Var x -> k (List.assoc x env)| Lam (x, body) -> k (Fn (fun v k' -> eval ((x, v) :: env) body k'))| App (f, a) -> eval env f (fun fv -> eval env a (fun av -> match fv with Fn g -> g av k | Int _ -> failwith "apply"))| Add (a, b) -> arith ( + ) env a b k| Mul (a, b) -> arith ( * ) env a b k| Callcc f ->(* The captured continuation, as a function, ignores the continuationof its own call and jumps to k. *)eval env f (function Fn g -> g (Fn (fun v _ -> k v)) k | Int _ -> failwith "callcc")and arith op env a b k =eval env a (fun x -> eval env b (fun y -> match (x, y) with Int x, Int y -> k (Int (op x y)) | _ -> failwith "arith"))let run e = match eval [] e Fun.id with Int n -> string_of_int n | Fn _ -> "<fun>"
Running it.
1 + callcc (fun k -> 10 * k 5) -> 61 + callcc (fun k -> 10 * 5) -> 51
In the first program the call k 5 discards the pending multiplication and returns 5 to the addition. The continuation is undelimited: it is the entire rest of the program. Delimited continuations capture only the part up to an enclosing delimiter; Danvy and Filinski's shift and reset are defined by a CPS transform applied twice[15]. OCaml 5's effect handlers are delimited continuations in which the handler is the delimiter, and they are implemented with separately allocated stack segments, not by CPS[16].
Logic
Under the Curry–Howard correspondence, the type translation with the answer type taken as falsity is a double-negation translation:
Double negation embeds classical logic into intuitionistic logic, and CPS embeds programs with control operators into programs without them. Griffin showed that call/cc has the type of Peirce's law, a classical tautology that has no intuitionistic proof[14]:
so adding call/cc to a typed λ-calculus makes its type system correspond to classical logic.
The continuation monad
The type is a monad:
and writing a program with it is writing it in CPS with the plumbing hidden. It is universal in a precise sense: any monad whose operations can be written in the language can be embedded in the continuation monad with a suitable answer type, which is how delimited continuations implement other effects[18].
Uses
Compilers
Steele's Rabbit compiler for Scheme converted programs to CPS before generating code[7], and Appel's Standard ML of New Jersey compiler used a CPS intermediate language throughout[8]. In CPS every intermediate value has a name, every control transfer is a call, and a function's return is a call to a known continuation, which makes inlining, dead-code elimination and register allocation uniform. Later compilers mostly use A-normal form, which records the same information without closures for continuations[10], or a CPS form with second-class continuations that are compiled as jumps[11].
Asynchronous programming
Callbacks in event-driven code are continuations written by hand: readFile(path, (err, data) => ...) in Node.js takes the rest of the computation as its last argument, and the error-first convention is the two-continuation pattern above. Deeply nested callbacks are CPS written out manually. Promises and async/await in JavaScript, C#, Python and Rust let the programmer write in direct style while the compiler performs a CPS-like transformation of the function into a state machine whose states are its continuations.
History
Continuations were discovered several times independently in the 1960s and early 1970s; Reynolds wrote a history of the discoveries[4]. Van Wijngaarden described a transformation of Algol 60 that eliminated procedure returns in 1964[5]. Strachey and Wadsworth used continuations to give a denotational semantics to jumps in 1974[3], and Fischer[6] and Reynolds[12] used them in 1972 to make an interpreter independent of the evaluation order of the language it was written in. Plotkin proved the transforms correct in 1975[2], and Sussman and Steele, in the report introducing Scheme the same year, coined the name continuation-passing style[1].
see also
- Tail callA tail call is a call made as the last action of a function, whose result the caller returns unchanged. Since the caller has nothing left to do, its stack frame can be reused, and the call compiled as a jump; a language that does this for every tail call is properly tail recursive, and loops can be written as recursion without the stack growing. OCaml, Scheme and most functional languages guarantee it; the JVM, Python and most JavaScript engines do not.
- Effect handlerAn effect handler runs code that may perform effects, and handles each effect by receiving it together with the continuation from the point where it was performed, which it can resume, discard or store. Handlers generalize exception handlers, and let generators, async I/O, schedulers, state and nondeterminism be written as libraries while the code that uses them stays in direct style. They come from the theory of algebraic effects of Plotkin, Power and Pretnar. OCaml 5 implements them with one-shot continuations: each can be resumed at most once, which keeps them cheap, since a continuation is the suspended stack itself.
- ClosureA closure is a function value together with the environment in which it was created: the bindings of the free variables its body refers to. Closures are what make functions first-class values in a lexically scoped language. Compilers implement them by closure conversion, which turns each function into a code pointer paired with a record of captured values.
- MonadIn functional programming, a monad is a type constructor m with two operations, return : a -> m a and bind : m a -> (a -> m b) -> m b, satisfying three laws. It lets code with some extra behaviour, such as failure, several results, configuration, state or I/O, be written as a sequence of ordinary steps, with the behaviour defined once in bind. The notion comes from category theory.
- Y combinatorThe Y combinator is a lambda term Y such that Y f = f (Y f) for every f: it finds a fixed point of any function, and so gives recursion to a language with no named definitions. Under call-by-value it loops before f is ever called; the Z combinator delays the self-application behind a lambda and works in strict languages. No fixed-point combinator has a type in the simply typed lambda calculus.
- β-reductionβ-reduction is the computation rule of the lambda calculus: an application of a function to an argument, (λx. e) a, is replaced by the body e with a substituted for x. Substitution must avoid capturing free variables of the argument. A term with no reducible subterm is in normal form; by the Church–Rosser theorem the normal form, if it exists, is unique, and normal-order reduction, which always contracts the leftmost outermost redex, finds it. Evaluation strategies of programming languages are restrictions of β-reduction.
- DefunctionalizationDefunctionalization is a whole-program transformation that removes higher-order functions. Each lambda expression in the program is replaced by a constructor of a data type, carrying the lambda's free variables, and each call of an unknown function is replaced by a call to a single first-order apply function that matches on the constructor and runs the corresponding body. Applied to the continuations of a program in continuation-passing style, it turns a recursive evaluator into an abstract machine whose stack is an explicit data structure.
- Green threadsGreen threads are threads scheduled by a language runtime or library instead of the operating system kernel. A suspended green thread is just a saved continuation or a small stack on the heap, so creating and switching between them costs little and a program can run hundreds of thousands at once. They are cooperative or preempted by the runtime at safe points, and they are multiplexed onto a smaller number of kernel threads (M:N scheduling), which requires blocking system calls to be turned into non-blocking ones. Erlang processes, Haskell threads, goroutines and Java virtual threads are examples.
- Parser combinatorA parser combinator is a higher-order function that builds a parser from smaller parsers: a sequence of two parsers, a choice between them, a repetition of one. Parsers are ordinary values in the host language, so a grammar is written as a program that is itself the parser, with no separate generator. Parser combinators produce recursive descent parsers; they cannot use left-recursive rules directly, and with unlimited backtracking can take exponential time, which memoization (packrat parsing) reduces to linear.
further reading
- [1]G. J. Sussman, G. L. Steele Jr., “Scheme: an interpreter for extended lambda calculus”, MIT AI Memo 349 (1975).
- [2]G. D. Plotkin, “Call-by-name, call-by-value and the λ-calculus”, Theoretical Computer Science 1 (1975).
- [3]C. Strachey, C. P. Wadsworth, “Continuations: a mathematical semantics for handling full jumps”, Oxford University Technical Monograph PRG-11 (1974).
- [4]J. C. Reynolds, “The discoveries of continuations”, Lisp and Symbolic Computation 6 (1993).
- [5]A. van Wijngaarden, “Recursive definition of syntax and semantics”, Formal Language Description Languages for Computer Programming, North-Holland (1966).
- [6]M. J. Fischer, “Lambda calculus schemata”, ACM Conference on Proving Assertions about Programs (1972).
- [7]G. L. Steele Jr., “Rabbit: a compiler for Scheme”, MIT AI Technical Report 474 (1978).
- [8]A. W. Appel, Compiling with Continuations, Cambridge University Press (1992).
- [9]O. Danvy, A. Filinski, “Representing control: a study of the CPS transformation”, Mathematical Structures in Computer Science 2 (1992).
- [10]C. Flanagan, A. Sabry, B. F. Duba, M. Felleisen, “The essence of compiling with continuations”, PLDI (1993).
- [11]A. Kennedy, “Compiling with continuations, continued”, ICFP (2007).
- [12]J. C. Reynolds, “Definitional interpreters for higher-order programming languages”, ACM Annual Conference (1972).
- [13]O. Danvy, L. R. Nielsen, “Defunctionalization at work”, PPDP (2001).
- [14]T. G. Griffin, “A formulae-as-types notion of control”, POPL (1990).
- [15]O. Danvy, A. Filinski, “Abstracting control”, LISP and Functional Programming (1990).
- [16]K. C. Sivaramakrishnan, S. Dolan, L. White, T. Kelly, S. Jaffer, A. Madhavapeddy, “Retrofitting effect handlers onto OCaml”, PLDI (2021).
- [17]M. S. Ager, D. Biernacki, O. Danvy, J. Midtgaard, “A functional correspondence between evaluators and abstract machines”, PPDP (2003).
- [18]A. Filinski, “Representing monads”, POPL (1994).
last updated