Recent from talks
Nothing was collected or created yet.
Continuation-passing style
View on WikipediaIn functional programming, continuation-passing style (CPS) is a style of programming in which control is passed explicitly in the form of a continuation. This is contrasted with direct style, which is the usual style of programming. Gerald Jay Sussman and Guy L. Steele, Jr. coined the phrase in AI Memo 349 (1975), which sets out the first version of the Scheme programming language.[1][2] John C. Reynolds gives a detailed account of the many discoveries of continuations.[3]
A function written in continuation-passing style takes an extra argument: an explicit continuation; i.e., a function of one argument. When the CPS function has computed its result value, it "returns" it by calling the continuation function with this value as the argument. That means that when invoking a CPS function, the calling function is required to supply a procedure to be invoked with the subroutine's "return" value. Expressing code in this form makes a number of things explicit which are implicit in direct style. These include: procedure returns, which become apparent as calls to a continuation; intermediate values, which are all given names; order of argument evaluation, which is made explicit; and tail calls, which simply call a procedure with the same continuation, unmodified, that was passed to the caller.
Programs can be automatically transformed from direct style to CPS. Compilers for functional and logic programming languages often use CPS as an intermediate representation where a compiler for an imperative or procedural programming language would use static single assignment form (SSA).[4] SSA is formally equivalent to a subset of CPS (excluding non-local control flow, which does not occur when CPS is used as intermediate representation).[5] Functional compilers can also use A-normal form (ANF) (but only for languages requiring eager evaluation), rather than with thunks (described in the examples below) in CPS. CPS is used more frequently by compilers than by programmers as a local or global style.
Examples
[edit]In CPS, each procedure takes an extra argument representing what should be done with the result the function is calculating. This, along with a restrictive style prohibiting a variety of constructs usually available, is used to expose the semantics of programs, making them easier to analyze. This style also makes it easy to express unusual control structures, like catch/throw or other non-local transfers of control.
The key to CPS is to remember that (a) every function takes an extra argument known as its continuation, and (b) every argument in a function call must be either a variable or a lambda expression (not a more complex expression). This has the effect of turning expressions "inside-out" because the innermost parts of the expression must be evaluated first, thus CPS makes explicit the order of evaluation as well as the control flow. Some examples of code in direct style and the corresponding CPS appear below. These examples are written in the programming language Scheme; by convention the continuation function is represented as a parameter named "k":
Direct style |
Continuation passing style
|
|---|---|
(define (pyth x y)
(sqrt (+ (* x x) (* y y))))
|
(define (pyth& x y k)
(*& x x (lambda (x2)
(*& y y (lambda (y2)
(+& x2 y2 (lambda (x2py2)
(sqrt& x2py2 k))))))))
|
(define (factorial n)
(if (= n 0)
1 ; NOT tail-recursive
(* n (factorial (- n 1)))))
|
(define (factorial& n k)
(=& n 0 (lambda (b)
(if b ; growing continuation
(k 1) ; in the recursive call
(-& n 1 (lambda (nm1)
(factorial& nm1 (lambda (f)
(*& n f k)))))))))
|
(define (factorial n)
(f-aux n 1))
(define (f-aux n a)
(if (= n 0)
a ; tail-recursive
(f-aux (- n 1) (* n a))))
|
(define (factorial& n k) (f-aux& n 1 k))
(define (f-aux& n a k)
(=& n 0 (lambda (b)
(if b ; unmodified continuation
(k a) ; in the recursive call
(-& n 1 (lambda (nm1)
(*& n a (lambda (nta)
(f-aux& nm1 nta k)))))))))
|
In the CPS versions, the primitives used, like +& and *& are themselves CPS, not direct style, so to make the above examples work in a Scheme system requires writing these CPS versions of primitives, with for instance *& defined by:
(define (*& x y k)
(k (* x y)))
To do this in general, we might write a conversion routine:
(define (cps-prim f)
(lambda args
(let ((r (reverse args)))
((car r) (apply f
(reverse (cdr r)))))))
(define *& (cps-prim *))
(define +& (cps-prim +))
To call a procedure written in CPS from a procedure written in direct style, it is necessary to provide a continuation that will receive the result computed by the CPS procedure. In the example above (assuming that CPS primitives have been provided), we might call (factorial& 10 (lambda (x) (display x) (newline))).
There is some variety between compilers in the way primitive functions are provided in CPS. Above is used the simplest convention, however sometimes Boolean primitives are provided that take two thunks to be called in the two possible cases, so the (=& n 0 (lambda (b) (if b ...))) call inside f-aux& definition above would be written instead as (=& n 0 (lambda () (k a)) (lambda () (-& n 1 ...))). Similarly, sometimes the if primitive is not included in CPS, and instead a function if& is provided which takes three arguments: a Boolean condition and the two thunks corresponding to the two arms of the conditional.
The translations shown above show that CPS is a global transformation. The direct-style factorial takes, as might be expected, a single argument; the CPS factorial& takes two: the argument and a continuation. Any function calling a CPS-ed function must either provide a new continuation or pass its own; any calls from a CPS-ed function to a non-CPS function will use implicit continuations. Thus, to ensure the total absence of a function stack, the entire program must be in CPS.
CPS in Haskell
[edit]A function pyth to calculate a hypotenuse using the Pythagorean theorem can be written in Haskell. A traditional implementation of the pyth function looks like this:
pow2 :: Float -> Float
pow2 x = x ** 2
add :: Float -> Float -> Float
add x y = x + y
pyth :: Float -> Float -> Float
pyth x y = sqrt (add (pow2 x) (pow2 y))
To transform the traditional function to CPS, its signature must be changed. The function will get another argument of function type, and its return type depends on that function:
pow2' :: Float -> (Float -> a) -> a
pow2' x cont = cont (x ** 2)
add' :: Float -> Float -> (Float -> a) -> a
add' x y cont = cont (x + y)
-- Types a -> (b -> c) and a -> b -> c are equivalent, so CPS function
-- may be viewed as a higher order function
sqrt' :: Float -> ((Float -> a) -> a)
sqrt' x = \cont -> cont (sqrt x)
pyth' :: Float -> Float -> (Float -> a) -> a
pyth' x y cont = pow2' x (\x2 -> pow2' y (\y2 -> add' x2 y2 (\anb -> sqrt' anb cont)))
First we calculate the square of a in pyth' function and pass a lambda function as a continuation which will accept a square of a as a first argument. And so on until the result of the calculations are reached. To get the result of this function we can pass id function as a final argument which returns the value that was passed to it unchanged: pyth' 3 4 id == 5.0.
The mtl library, which is shipped with Glasgow Haskell Compiler (GHC), has the module Control.Monad.Cont. This module provides the Cont type, which implements Monad and some other useful functions. The following snippet shows the pyth' function using Cont:
pow2_m :: Float -> Cont a Float
pow2_m a = return (a ** 2)
pyth_m :: Float -> Float -> Cont a Float
pyth_m a b = do
a2 <- pow2_m a
b2 <- pow2_m b
anb <- cont (add' a2 b2)
r <- cont (sqrt' anb)
return r
Not only has the syntax become cleaner, but this type allows us to use a function callCC with type MonadCont m => ((a -> m b) -> m a) -> m a. This function has one argument of a function type; that function argument accepts the function too, which discards all computations going after its call. For example, let's break the execution of the pyth function if at least one of its arguments is negative returning zero:
pyth_m :: Float -> Float -> Cont a Float
pyth_m a b = callCC $ \exitF -> do -- $ sign helps to avoid parentheses: a $ b + c == a (b + c)
when (b < 0 || a < 0) (exitF 0.0) -- when :: Applicative f => Bool -> f () -> f ()
a2 <- pow2_m a
b2 <- pow2_m b
anb <- cont (add' a2 b2)
r <- cont (sqrt' anb)
return r
Continuations as objects
[edit]Programming with continuations can also be useful when a caller does not want to wait until the callee completes. For example, in user interface (UI) programming, a routine can set up dialog box fields and pass these, along with a continuation function, to the UI framework. This call returns right away, allowing the application code to continue while the user interacts with the dialog box. Once the user presses the "OK" button, the framework calls the continuation function with the updated fields. Although this style of coding uses continuations, it is not full CPS.[clarification needed]
function confirmName() {
fields.name = name;
framework.Show_dialog_box(fields, confirmNameContinuation);
}
function confirmNameContinuation(fields) {
name = fields.name;
}
A similar idea can be used when the function must run in a different thread or on a different processor. The framework can execute the called function in a worker thread, then call the continuation function in the original thread with the worker's results. This is in Java 8 using the Swing UI framework:
void buttonHandler() {
// This is executing in the Swing UI thread.
// We can access UI widgets here to get query parameters.
int parameter = getField();
new Thread(() -> {
// This code runs in a separate thread.
// We can do things like access a database or a
// blocking resource like the network to get data.
int result = lookup(parameter);
javax.swing.SwingUtilities.invokeLater(() -> {
// This code runs in the UI thread and can use
// the fetched data to fill in UI widgets.
setField(result);
});
}).start();
}
Tail calls
[edit]Every call in CPS is a tail call, and the continuation is explicitly passed. Using CPS without tail call optimization (TCO) will cause both the constructed continuation to potentially grow during recursion, and the call stack. This is usually undesirable, but has been used in interesting ways; see the Chicken Scheme compiler. As CPS and TCO eliminate the concept of an implicit function return, their combined use can eliminate the need for a run-time stack. Several compilers and interpreters for functional programming languages use this ability in novel ways.[6]
Use and implementation
[edit]Continuation passing style can be used to implement continuations and control flow operators in a functional language that does not feature first-class continuations but does have first-class functions and tail-call optimization. Without tail-call optimization, techniques such as trampolining, i.e., using a loop that iteratively invokes thunk-returning functions, can be used; without first-class functions, it is even possible to convert tail calls into just gotos in such a loop.
Writing code in CPS, while not impossible, is often error-prone. There are various translations, usually defined as one- or two-pass conversions of pure lambda calculus, which convert direct style expressions into CPS expressions. Writing in trampolined style, however, is extremely difficult; when used, it is usually the target of some sort of transformation, such as compilation.
Functions using more than one continuation can be defined to capture various control flow paradigms, for example (in Scheme):
(define (/& x y ok err)
(=& y 0.0 (lambda (b)
(if b
(err (list "div by zero!" x y))
(ok (/ x y))))))
A CPS transform is conceptually a Yoneda embedding.[7] It is also similar to the embedding of lambda calculus in π-calculus.[8][9]
Use in other fields
[edit]Outside of computer science, CPS is of more general interest as an alternative to the conventional method of composing simple expressions into complex expressions. For example, within linguistic semantics, Chris Barker and his collaborators have suggested that specifying the denotations of sentences using CPS might explain certain phenomena in natural language.[10]
In mathematics, the Curry–Howard isomorphism between computer programs and mathematical proofs relates continuation-passing style translation to a variation of double-negation embeddings of classical logic into intuitionistic (constructive) logic. Unlike the regular double-negation translation, which maps atomic propositions p to ((p → ⊥) → ⊥), the continuation passing style replaces ⊥ by the type of the final expression. Accordingly, the result is obtained by passing the identity function as a continuation to the CPS expression, as in the above example.
Classical logic itself relates to manipulating the continuation of programs directly, as in Scheme's call-with-current-continuation control operator, an observation due to Tim Griffin (using the closely related C control operator).[11]
See also
[edit]Notes
[edit]- ^ Sussman, Gerald Jay; Steele, Guy L. Jr. (December 1975). . AI Memo. 349: 19.
That is, in this continuation-passing programming style, a function always "returns" its result by "sending" it to another function. This is the key idea.
- ^ Sussman, Gerald Jay; Steele, Guy L. Jr. (December 1998). "Scheme: A Interpreter for Extended Lambda Calculus" (reprint). Higher-Order and Symbolic Computation. 11 (4): 405–439. doi:10.1023/A:1010035624696. S2CID 18040106.
We believe that this was the first occurrence of the term "continuation-passing style" in the literature. It has turned out to be an important concept in source code analysis and transformation for compilers and other metaprogramming tools. It has also inspired a set of other "styles" of program expression.
- ^ Reynolds, John C. (1993). "The Discoveries of Continuations". LISP and Symbolic Computation. 6 (3–4): 233–248. CiteSeerX 10.1.1.135.4705. doi:10.1007/bf01019459. S2CID 192862.
- ^ Appel, Andrew W. (April 1998). "SSA is Functional Programming". ACM SIGPLAN Notices. 33 (4): 17–20. CiteSeerX 10.1.1.34.3282. doi:10.1145/278283.278285. S2CID 207227209.
- ^ Kelsey, Richard A. (March 1995). "A Correspondence between Continuation Passing Style and Static Single Assignment Form". ACM SIGPLAN Notices. 30 (3): 13–22. CiteSeerX 10.1.1.489.930. doi:10.1145/202530.202532.
- ^ Appel, Andrew W. (1992). Compiling with Continuations. Cambridge University Press. ISBN 0-521-41695-7.
- ^ Stay, Mike. The Continuation Passing Transform and the Yoneda Embedding (Report).
- ^ Mike Stay, "The Pi Calculus II"
- ^ Boudol, Gérard (1997). "The π-Calculus in Direct Style". CiteSeerX 10.1.1.52.6034.
- ^ Barker, Chris (2002-09-01). "Continuations and the Nature of Quantification" (PDF). Natural Language Semantics. 10 (3): 211–242. doi:10.1023/A:1022183511876. ISSN 1572-865X. S2CID 118870676.
- ^ Griffin, Timothy (January 1990). "A formulae-as-type notion of control". Proceedings of the 17th ACM SIGPLAN-SIGACT symposium on Principles of programming languages - POPL '90. Vol. 17. pp. 47–58. doi:10.1145/96709.96714. ISBN 978-0-89791-343-0. S2CID 3005134.
References
[edit]- Continuation Passing C (CPC) - programming language for writing concurrent systems, designed and developed by Juliusz Chroboczek and Gabriel Kerneis. github repository
- The construction of a CPS-based compiler for ML is described in: Appel, Andrew W. (1992). Compiling with Continuations. Cambridge University Press. ISBN 978-0-521-41695-5.
- Danvy, Olivier; Filinski, Andrzej (1992). "Representing Control, A Study of the CPS Transformation". Mathematical Structures in Computer Science. 2 (4): 361–391. CiteSeerX 10.1.1.46.84. doi:10.1017/S0960129500001535. S2CID 8790522.
- Chicken Scheme compiler, a Scheme to C compiler that uses continuation-passing style for translating Scheme procedures into C functions while using the C-stack as the nursery for the generational garbage collector
- Kelsey, Richard A. (March 1995). "A Correspondence between Continuation Passing Style and Static Single Assignment Form". ACM SIGPLAN Notices. 30 (3): 13–22. CiteSeerX 10.1.1.3.6773. doi:10.1145/202530.202532.
- Appel, Andrew W. (April 1998). "SSA is Functional Programming". ACM SIGPLAN Notices. 33 (4): 17–20. CiteSeerX 10.1.1.34.3282. doi:10.1145/278283.278285. S2CID 207227209.
- Danvy, Olivier; Millikin, Kevin; Nielsen, Lasse R. (2007). "On One-Pass CPS Transformations". BRICS Report Series: 24. ISSN 0909-0878. RS-07-6. Retrieved 26 October 2007.
- Dybvig, R. Kent (2003). The Scheme Programming Language. Prentice Hall. p. 64. Direct link: "Section 3.4. Continuation Passing Style".
Continuation-passing style
View on Grokipediaadd_cps(x, y, k) = k(x + y), where k is the continuation that processes the sum.[1] This transformation, often called CPS conversion, is systematic and can be applied mechanically to direct-style code, resulting in a form that is easier to analyze or optimize, particularly for tail calls which become direct jumps.[3]
CPS has found wide application in compiler design as an intermediate representation, facilitating optimizations like closure conversion and defunctionalization.[3] In Scheme, it underpins the implementation of the call/cc (call-with-current-continuation) operator, which captures and manipulates continuations at runtime; CPS also serves as a key intermediate form in compilers for languages like ML.[4] More broadly, CPS influences modern paradigms, including the continuation monad for handling computational effects in functional languages and asynchronous programming patterns in JavaScript, where promises can be viewed as resembling delimited continuations.[5][6] Its mathematical foundations connect to category theory, notably the Yoneda lemma, underscoring its role in embedding classical control into intuitionistic logic via double negation translations.[7]
Fundamentals
Definition
Continuation-passing style (CPS) is a functional programming technique in which functions are structured to accept an extra argument known as the continuation, representing the remaining computation to be performed after the function yields its result. Instead of returning values directly via implicit mechanisms like the call stack, a function in CPS explicitly invokes its continuation argument with the computed result, thereby making the program's control flow transparent and compositional. In this style, a function that normally takes input and produces output is transformed into a form , where denotes the continuation—a function that accepts and proceeds with the rest of the program, typically by calling . This approach ensures that all potential return points and control transfers are represented as first-class function values passed explicitly. A key advantage of CPS is its ability to eliminate hidden stack-based returns, allowing for explicit management of computation sequences and enabling optimizations like tail-call elimination in supporting environments.Direct style comparison
In direct-style programming, functions implicitly return values through the call stack, allowing control to flow naturally from the caller to the callee without explicitly managing post-computation actions.[8] This approach relies on the runtime environment to handle evaluation order and returns, making it intuitive for sequential code but dependent on the underlying evaluation strategy, such as call-by-value or call-by-name.[9] Continuation-passing style (CPS) transforms this implicit control into an explicit mechanism by converting direct-style code such that every function receives an additional argument representing its continuation—a function that processes the result once computation completes.[8] For instance, a direct-style functionf(x) that returns a value becomes f(x, k) in CPS, where k is the continuation, often starting as the identity function for the top-level result.[10] This transformation, formalized in the relationship between direct and CPS semantics, ensures that evaluation order is independent of the host language's strategy, as control is passed explicitly via continuations.[11]
The explicit handling of continuations in CPS offers advantages over direct style, such as facilitating the implementation of advanced control features like coroutines by treating pending computations as reified objects that can be suspended or resumed.[9] However, CPS introduces drawbacks, including increased code verbosity due to the pervasive passing of continuation arguments and potential loss of tail recursion optimization benefits unless the implementation explicitly supports tail calls to continuations.[9] Tail calls, which enable loop-like efficiency in both styles, are preserved in CPS through its inherent structure of final continuation invocations.[11]
To illustrate the contrast, consider a simple addition function. In direct style:
add(a, b) = a + b
add(a, b) = a + b
add(a, b, k) = k(a + b)
add(a, b, k) = k(a + b)
k receives the sum and determines the next step, making control flow explicit at the cost of additional parameters.[8]
Examples
In Scheme
In Scheme, the primitivecall-with-current-continuation, commonly abbreviated as call/cc, captures the current continuation of a computation and passes it as an argument to a given procedure.[12] This procedure takes one argument, a function of one argument, and invokes it with the captured continuation, which is itself a first-class procedure that, when applied to a value, restores the control context at the point of capture and delivers that value as the result of the call/cc expression.[12] Scheme's treatment of continuations as first-class objects enables their reification and manipulation, facilitating the implementation of continuation-passing style (CPS) by allowing explicit passing and invocation of continuations without relying on implicit stack management.[12]
A straightforward example of CPS in Scheme is the factorial function, which avoids traditional returns by passing an explicit continuation to handle the result. The CPS version is defined as follows:
(define fact-cps
(lambda (n k)
(if (= n 0)
(k 1)
(fact-cps (- n 1) ([lambda](/page/Lambda) (v) (k (* n v)))))))
(define fact-cps
(lambda (n k)
(if (= n 0)
(k 1)
(fact-cps (- n 1) ([lambda](/page/Lambda) (v) (k (* n v)))))))
(fact-cps 5 ([lambda](/page/Lambda) (v) v)) computes 120 by chaining nested continuations, ensuring tail-recursive calls that prevent stack overflow in Scheme implementations supporting proper tail calls.[13] This style transforms recursive computations into iterative ones mediated by continuations, leveraging Scheme's functional nature.
The call/cc primitive also supports non-local control transfers, such as implementing error handling akin to try-catch mechanisms. For instance, consider a procedure that may abort on error:
(define (divide x y)
(call/cc
(lambda (escape)
(if (= y 0)
(escape 'division-by-zero)
(/ x y)))))
(define (divide x y)
(call/cc
(lambda (escape)
(if (= y 0)
(escape 'division-by-zero)
(/ x y)))))
y is zero, (escape 'division-by-zero) invokes the captured continuation, immediately returning 'division-by-zero from the call/cc expression and bypassing the remainder of the computation, thus providing a dynamic escape route without unwinding the stack manually.[14] This capability underscores how Scheme's first-class continuations empower CPS to model exceptional control flow explicitly.[12]
In Haskell
In Haskell, continuation-passing style (CPS) provides a mechanism for explicitly managing control flow in pure functional programs, where functions accept an additional continuation argument representing the computation to perform with the result, rather than returning values directly. This approach is particularly useful in Haskell's statically typed environment, enabling optimizations without relying on runtime continuation capture mechanisms.[15] A CPS function in Haskell typically has the type signaturea -> (b -> r) -> r, where a is the input type, b is the intermediate result type passed to the continuation, and r is the overall result type of the enclosing computation.[16]
An illustrative example of CPS in Haskell is a continuation-based list reversal, which can be implemented recursively to ensure tail recursion and linear-time performance:
reverseCPS :: [a] -> ([a] -> r) -> r
reverseCPS [] k = k []
reverseCPS (x:xs) k = reverseCPS xs (\ys -> k (ys ++ [x]))
reverseCPS :: [a] -> ([a] -> r) -> r
reverseCPS [] k = k []
reverseCPS (x:xs) k = reverseCPS xs (\ys -> k (ys ++ [x]))
ys to the outer continuation k, avoiding stack growth through Haskell's tail-call optimization in CPS form.[17]
CPS also underpins Haskell's handling of monadic effects, particularly through monad transformers like ContT, which sequence computations in continuation-passing style within broader monadic stacks, allowing delimited control over effectful operations.[18]
As first-class objects
In languages supporting continuation-passing style (CPS) with first-class continuations, such as Scheme, a continuation can be captured and treated as a value that is assignable to variables, storable in data structures, or passed as arguments to other functions. This reification is typically achieved through a primitive likecall-with-current-continuation (abbreviated call/cc), which packages the current continuation as an escape procedure and passes it to a provided function. The resulting continuation is a first-class procedure with unlimited extent, meaning it can be invoked at any time to resume the captured computation from the point of capture, abandoning the current continuation in favor of the saved one.[12]
For instance, in Scheme, a continuation can be stored for later invocation as follows:
(define cont (call/cc ([lambda](/page/Lambda) (k) k)))
(define cont (call/cc ([lambda](/page/Lambda) (k) k)))
cont holds the identity continuation, and invoking (cont value) later resumes the computation by passing value to the original context, effectively returning value as if from the call/cc point. This demonstrates how continuations become manipulable objects rather than hidden runtime mechanisms.[12]
Treating continuations as first-class objects enables explicit manipulation for advanced control structures, such as implementing threads via multiple interleaved continuations, exceptions through non-local jumps to handler continuations, and backtracking by reinstating prior states in search algorithms. In contrast, direct-style programming keeps continuations implicit and non-manipulable, limiting programmers to the language's built-in control flow without such flexibility.[19] Delimited continuations offer a bounded variant of this capability, restricting captures to a specified context rather than the full program stack.[12]
Control Flow
Tail calls
In continuation-passing style (CPS), a tail call occurs when a function's final action is to invoke a continuation with its result, leaving no additional computation to perform in the current activation frame.[20] This structure explicitly represents the "rest of the computation" as the continuation parameter, ensuring that control flow passes directly to the successor without intermediate steps.[21] Tail call optimization (TCO) leverages this property by allowing compilers to reuse the current stack frame for the continuation invocation, preventing stack growth and converting recursive calls into iterative loops with constant space usage.[20] For instance, in a recursive summation function written in CPS, each step accumulates the result and tails to the next continuation, enabling the optimizer to avoid allocating new frames and thus supporting unbounded recursion without overflow.[22] Consider the following pseudocode for a CPS version of a list summation function:define sum-cps xs k =
if null? xs then
k 0
else
let x = [car](/page/Car) xs
in sum-cps (cdr xs) ([lambda](/page/Lambda) y . k (+ x y))
define sum-cps xs k =
if null? xs then
k 0
else
let x = [car](/page/Car) xs
in sum-cps (cdr xs) ([lambda](/page/Lambda) y . k (+ x y))
sum-cps is in tail position, passing a new continuation that adds the current element; TCO transforms this into a loop, preserving stack space across iterations.[20]
The CPS transformation process preserves tail positions from direct-style code by mapping them directly to continuation applications, ensuring no extra stack frames are introduced for proper tail calls.[21] This alignment facilitates optimizations like β- and η-reductions, where tail continuations simplify to direct jumps in the compiled code.[21]
Delimited continuations
Delimited continuations represent a restricted variant of continuation-passing style where the captured continuation is limited to the portion from the current execution point up to a dynamically specified boundary, or delimiter, rather than encompassing the entire remaining program execution as in undelimited continuations. This delimitation allows for more precise control over program flow, enabling the programmer to abstract and manipulate only a bounded segment of the computation context. The concept originates from efforts to refine the expressive power of continuations while mitigating their potential for unintended global effects.[23] In languages like Scheme, delimited continuations are typically implemented using the operators shift and reset. The reset operator establishes a delimiter, evaluating its body within a bounded context and ensuring that any captured continuations do not extend beyond this point; it effectively reifies the computation inside it as a value, discarding the outer context upon completion. The shift operator, in contrast, captures the delimited continuation up to the nearest enclosing reset and binds it to a parameter (often denoted as ), which is then passed to its body expression; this body can invoke multiple times or compose it with other computations, but invocations respect the delimiter and do not affect the surrounding code. These operators work in tandem to provide first-class, composable representations of partial continuations that behave like ordinary functions.[24] A illustrative example in Scheme is the expression(reset (+ 1 (shift k (k 2)))), which evaluates to 3. Here, shift captures the continuation corresponding to the addition of 1 within the reset boundary; invoking with 2 applies this partial continuation, yielding 3, while the delimiter prevents any further propagation of control outside the reset. This demonstrates how shift abstracts the local context for reuse without altering the broader program structure.[25]
Delimited continuations offer several advantages over full, undelimited continuations, including enhanced safety by restricting capture to avoid unintended stack inclusion or global side effects, which can lead to non-termination or erroneous control flow in larger programs. They facilitate modular reasoning about effects, as the bounded scope supports local handling and composition of control abstractions without pervasive impacts. Additionally, their design enables uniform simulation of computational effects, such as monads, with efficient, low-overhead implementations derived directly from continuation-passing style transformations, promoting composability in functional programming paradigms.[24]
Implementation
Code transformation
Continuation-passing style (CPS) code transformation is a mechanical, recursive process that converts direct-style functional programs into an equivalent CPS form, making control flow explicit by parameterizing functions with continuations. This technique, foundational to compilers for languages like Scheme and ML, enables optimizations such as tail-call elimination and defunctionalization by representing all computations as tail calls to continuations. The transformation applies to call-by-value λ-calculus and ensures that every value-producing expression passes its result to a continuation, eliminating implicit returns.[26] The core transformation rules define how each syntactic construct is mapped to CPS. For a variablex, the CPS form is a function that applies the continuation k to x:
[[x]] = λk. k(x)
[[x]] = λk. k(x)
f(e₁, ..., eₙ), the CPS form is:
[[f(e₁, ..., eₙ)]] = λk. [[f]](λv_f. [[e₁]](λv₁. ⋯ [[eₙ]](λvₙ. v_f(v₁, ..., vₙ, k)) ⋯ ))
[[f(e₁, ..., eₙ)]] = λk. [[f]](λv_f. [[e₁]](λv₁. ⋯ [[eₙ]](λvₙ. v_f(v₁, ..., vₙ, k)) ⋯ ))
v_f, v₁, ..., vₙ, and k are introduced to bind intermediate results, ensuring left-to-right evaluation order.[26]
Lambda abstractions are transformed by adding a continuation parameter to the function body. For λx. e, the CPS form becomes:
[[λx. e]] = λk. k(λx k'. [[e]] k')
[[λx. e]] = λk. k(λx k'. [[e]] k')
e is recursively converted, and the resulting closure takes both the original parameter x and a fresh continuation k'. This rule embeds the function as a value that expects a continuation upon application.[26]
Let-bindings are handled by converting them into nested applications of the bound expression to a continuation that substitutes the value into the body. For let x = e in body, the CPS form is:
[[let x = e in body]] = λk. [[e]](λv. let x = v in [[body]] k)
[[let x = e in body]] = λk. [[e]](λv. let x = v in [[body]] k)
e to a value v, binds it to x, and continues with the transformed body, preserving scoping and avoiding redundant computations.[27]
The overall algorithm for the transformation is a recursive traversal of the program's abstract syntax tree, typically implemented in a single pass for efficiency. It proceeds as follows: (1) For each expression, introduce a fresh continuation variable k as the parameter to the enclosing λ-abstraction; (2) Recursively apply the rules to subexpressions, propagating k downward and binding intermediate values with fresh variables; (3) Ensure every path through the expression concludes with a tail call to k applied to the computed value, eliminating any implicit returns or branches that do not invoke the continuation. This approach avoids generating unnecessary "administrative" redexes through careful fresh variable allocation and can incorporate optimizations like reference counting for linear-time execution.[28]
The transformation preserves the semantics of the original program: executing the CPS version with the identity continuation λx. x yields the same observable behavior as the direct-style code, including side effects and non-termination. This equivalence holds under operational semantics and is proven using techniques such as logical relations, ensuring that the CPS form is a faithful intermediate representation suitable for further compilation stages. Additionally, the process preserves tail positions from the source, facilitating stack-safe recursion in the output.
Handling non-local control
In continuation-passing style (CPS), non-local exits are managed by explicitly passing special continuations, such as abort handlers or error continuations, as additional arguments to functions, allowing control to jump outside the normal return path without relying on implicit stack unwinding.[29] This approach models abrupt control transfers, like gotos or early returns, by invoking the designated handler continuation directly with the relevant value or state.[30] Exceptions in CPS are typically implemented using a double-barrelled style, where each function receives both a success continuation (for normal returns) and an exception continuation (for error cases), often written as .[31] For instance, araise e operation evaluates to a value and invokes , bypassing the success path, while a try e1 catch x => e2 installs a local handler by evaluating with an updated that binds to the raised value and proceeds to .[32] This dual-continuation passing simulates try-catch semantics without altering the underlying CPS transformation, ensuring exceptions propagate to the appropriate handler.
;; Example: Dual-continuation for [exception handling](/page/Exception_handling) in Scheme-like CPS
(define (safe-div x y k h)
(if (zero? y)
(h "division by zero") ; Invoke exception continuation
(k (/ x y)))) ; Invoke success continuation
(define (try-div x y k_outer h_outer)
(safe-div x y
(lambda (result) (k_outer result)) ; Success: continue normally
(lambda (err) ;; Local catch handler: bind err (as x) and proceed to e2 (e.g., return 0)
(k_outer 0)))) ; Simulate catch returning 0 on error
;; Example: Dual-continuation for [exception handling](/page/Exception_handling) in Scheme-like CPS
(define (safe-div x y k h)
(if (zero? y)
(h "division by zero") ; Invoke exception continuation
(k (/ x y)))) ; Invoke success continuation
(define (try-div x y k_outer h_outer)
(safe-div x y
(lambda (result) (k_outer result)) ; Success: continue normally
(lambda (err) ;; Local catch handler: bind err (as x) and proceed to e2 (e.g., return 0)
(k_outer 0)))) ; Simulate catch returning 0 on error
Applications
In functional programming
Continuation-passing style (CPS) plays a central role in the denotational semantics of functional programming languages, where it provides a mathematical framework for interpreting lambda calculus and related constructs by explicitly passing continuations as arguments to functions. This approach, pioneered in early works on semantics, models computation as a series of transformations on continuations, enabling precise definitions of evaluation order and control flow without relying on operational details like stacks. For instance, in denotational semantics, the meaning of an expression is a function that takes a continuation and produces a result by applying it, as formalized in treatments of typed lambda calculus where computation types are defined as , with as the continuation type for value type and as the response type.[34][35] In interpreters for functional languages, CPS serves as an effective intermediate representation (IR) for evaluating lambda terms, facilitating optimizations and clear separation of value and control aspects. Compilers for functional languages, including historical experiments in the Glasgow Haskell Compiler (GHC), have employed CPS as an IR to transform direct-style code into a form where all control flow is explicit, aiding in tasks like strictness analysis and code generation for lambda calculus evaluation. This transformation ensures that function applications become tail calls in the CPS form, which is crucial for efficient recursive functional code.[9][36] CPS forms the foundational basis for monads in functional programming, particularly in handling effects like I/O and state through continuation-based structures. The continuation monad in Haskell, defined asnewtype Cont r a = Cont { runCont :: (a -> r) -> r }, directly embodies CPS by representing computations that take a continuation of type a -> r and yield a final result of type r, allowing monadic composition of effectful operations while preserving referential transparency. This monad underpins implementations of state and I/O monads, where the continuation captures the residual computation, enabling delimited control over effects in pure functional settings.[37]
In functional programming, CPS enables the implementation of non-deterministic constructs like the amb operator for backtracking search, such as in parsers or logic programming, by reifying continuations to capture and resume alternative computation paths. The amb operator, which introduces ambiguous choice, can be realized using first-class continuations to implement failure and backtracking: upon failure, the continuation is rewound to the last choice point, exploring alternatives in a depth-first manner without explicit search structures. This approach, rooted in early nondeterministic programming paradigms, integrates seamlessly with CPS interpreters for efficient evaluation of ambiguous expressions in functional contexts.[38][39]
