Hubbry Logo
Continuation-passing styleContinuation-passing styleMain
Open search
Continuation-passing style
Community hub
Continuation-passing style
logo
7 pages, 0 posts
0 subscribers
Be the first to start a discussion here.
Be the first to start a discussion here.
Continuation-passing style
Continuation-passing style
from Wikipedia

In 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]

References

[edit]
Revisions and contributorsEdit on WikipediaRead on Wikipedia
from Grokipedia
Continuation-passing style (CPS) is a programming technique used primarily in , where functions are transformed to accept an additional argument known as a continuation—a function that represents the remaining to be performed upon receiving the function's result. Rather than directly returning values, functions in CPS invoke the continuation with their computed result, thereby making explicit and avoiding implicit returns or stack-based mechanisms. This style ensures that the order of evaluation can be precisely controlled without relying on the underlying runtime environment's . The origins of CPS trace back to the early in the development of formal semantics for programming languages. John C. Reynolds introduced the concept in his 1972 paper on definitional interpreters for higher-order programming languages, where CPS was employed to eliminate dependencies on the order of in interpreters, allowing for order-independent while preserving higher-order features. Building on this, and Christopher Wadsworth formalized in 1974 as state transformations in , enabling the modeling of jumps and control structures like exceptions in pure functional settings by representing the "rest of the program" as a discardable continuation. Key features of CPS include its ability to express arbitrary control flows, such as non-local exits or coroutines, within a purely functional framework without side effects. For instance, in CPS, a simple arithmetic operation like might be rewritten as add_cps(x, y, k) = k(x + y), where k is the that processes the sum. 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. CPS has found wide application in compiler design as an , facilitating optimizations like closure conversion and defunctionalization. In Scheme, it underpins the implementation of the call/cc (call-with-current-continuation) operator, which captures and manipulates at runtime; CPS also serves as a key intermediate form in compilers for languages like ML. More broadly, CPS influences modern paradigms, including the monad for handling computational effects in functional languages and asynchronous programming patterns in , where promises can be viewed as resembling delimited . Its mathematical foundations connect to , notably the , underscoring its role in embedding classical control into via translations.

Fundamentals

Definition

Continuation-passing style (CPS) is a technique in which functions are structured to accept an extra argument known as the , 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 argument with the computed result, thereby making the program's transparent and compositional. In this style, a function that normally takes input xx and produces output vv is transformed into a form f(x,k)f(x, k), where kk denotes the —a function that accepts vv and proceeds with the rest of the program, typically by calling k(v)k(v). This approach ensures that all potential return points and control transfers are represented as 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. This approach relies on the runtime environment to handle evaluation order and returns, making it intuitive for sequential code but dependent on the underlying , such as call-by-value or call-by-name. 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 —a function that processes the result once completes. For instance, a direct-style function f(x) that returns a value becomes f(x, k) in CPS, where k is the , often starting as the for the top-level result. 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. 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. 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. Tail calls, which enable loop-like efficiency in both styles, are preserved in CPS through its inherent structure of final continuation invocations. To illustrate the contrast, consider a simple function. In direct style:

add(a, b) = a + b

add(a, b) = a + b

This returns the sum directly via the call stack. In CPS:

add(a, b, k) = k(a + b)

add(a, b, k) = k(a + b)

Here, the continuation k receives the sum and determines the next step, making explicit at the cost of additional parameters.

Examples

In Scheme

In Scheme, the primitive call-with-current-continuation, commonly abbreviated as call/cc, captures the current of a and passes it as an argument to a given procedure. This procedure takes one argument, a function of one argument, and invokes it with the captured , 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. Scheme's treatment of as first-class objects enables their reification and manipulation, facilitating the implementation of continuation-passing style (CPS) by allowing explicit passing and invocation of without relying on implicit stack management. A straightforward example of CPS in Scheme is the function, which avoids traditional returns by passing an explicit 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)))))))

Invoking (fact-cps 5 ([lambda](/page/Lambda) (v) v)) computes 120 by chaining nested continuations, ensuring tail-recursive calls that prevent in Scheme implementations supporting proper tail calls. 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 handling akin to try-catch mechanisms. For instance, consider a procedure that may abort on :

(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)))))

Here, if y is zero, (escape 'division-by-zero) invokes the captured , immediately returning 'division-by-zero from the call/cc expression and bypassing the of the computation, thus providing a dynamic escape route without unwinding the stack manually. This capability underscores how Scheme's first-class continuations empower CPS to model exceptional explicitly.

In Haskell

In Haskell, continuation-passing style (CPS) provides a mechanism for explicitly managing in pure functional programs, where functions accept an additional continuation 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. A CPS function in typically has the type signature a -> (b -> r) -> r, where a is the input type, b is the intermediate result type passed to the , and r is the overall result type of the enclosing computation. An illustrative example of CPS in Haskell is a continuation-based reversal, which can be implemented recursively to ensure tail and linear-time performance:

haskell

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]))

This formulation passes the accumulated reversed prefix ys to the outer k, avoiding stack growth through 's tail-call optimization in CPS form. CPS also underpins '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.

As first-class objects

In languages supporting continuation-passing style (CPS) with first-class s, such as Scheme, a 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 like call-with-current-continuation (abbreviated call/cc), which packages the current as an escape procedure and passes it to a provided function. The resulting 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 in favor of the saved one. For instance, in Scheme, a 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)))

Here, cont holds the identity , 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. 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 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 without such flexibility. Delimited continuations offer a bounded variant of this capability, restricting captures to a specified context rather than the full program stack.

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. This structure explicitly represents the "rest of the computation" as the continuation parameter, ensuring that passes directly to the successor without intermediate steps. Tail call optimization (TCO) leverages this property by allowing compilers to reuse the current stack frame for the invocation, preventing stack growth and converting recursive calls into iterative loops with constant space usage. For instance, in a recursive function written in CPS, each step accumulates the result and tails to the next , enabling the optimizer to avoid allocating new frames and thus supporting unbounded without overflow. 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))

Here, the recursive call to sum-cps is in tail position, passing a new that adds the current element; TCO transforms this into a loop, preserving stack space across iterations. The CPS transformation process preserves tail positions from direct-style code by mapping them directly to applications, ensuring no extra stack frames are introduced for proper tail calls. This alignment facilitates optimizations like β- and η-reductions, where tail simplify to direct jumps in the compiled code.

Delimited continuations

Delimited continuations represent a restricted variant of continuation-passing style where the captured is limited to the portion from the current execution point up to a dynamically specified boundary, or , 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 . The originates from efforts to refine the expressive power of continuations while mitigating their potential for unintended global effects. 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 kk), which is then passed to its body expression; this body can invoke kk 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. A illustrative example in Scheme is the expression (reset (+ 1 (shift k (k 2)))), which evaluates to 3. Here, shift captures the kk corresponding to the of 1 within the reset boundary; invoking kk with 2 applies this partial , yielding 3, while the 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. 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 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 of computational effects, such as monads, with efficient, low-overhead implementations derived directly from continuation-passing style transformations, promoting in paradigms.

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 explicit by parameterizing functions with . 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 . The transformation applies to call-by-value λ-calculus and ensures that every value-producing expression passes its result to a continuation, eliminating implicit returns. The core transformation rules define how each syntactic construct is mapped to CPS. For a variable x, the CPS form is a function that applies the continuation k to x:

[[x]] = λk. k(x)

[[x]] = λk. k(x)

This rule simply forwards the variable's value to the continuation. For function applications, the rule evaluates the operator and operands sequentially, then applies the operator to the operands and the continuation. For an application 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)) ⋯ ))

Here, fresh variables v_f, v₁, ..., vₙ, and k are introduced to bind intermediate results, ensuring left-to-right evaluation order. Lambda abstractions are transformed by adding a 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')

The body e is recursively converted, and the resulting closure takes both the original parameter x and a fresh k'. This rule embeds the function as a value that expects a upon application. 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)

This evaluates e to a value v, binds it to x, and continues with the transformed body, preserving scoping and avoiding redundant computations. The overall algorithm for the transformation is a recursive traversal of the program's , typically implemented in a single pass for efficiency. It proceeds as follows: (1) For each expression, introduce a fresh 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 to k applied to the computed value, eliminating any implicit returns or branches that do not invoke the . This approach avoids generating unnecessary "administrative" redexes through careful fresh variable allocation and can incorporate optimizations like for linear-time execution. 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 and is proven using techniques such as logical relations, ensuring that the CPS form is a faithful suitable for further compilation stages. Additionally, the process preserves tail positions from the source, facilitating stack-safe 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. This approach models abrupt control transfers, like gotos or early returns, by invoking the designated handler continuation directly with the relevant value or state. Exceptions in CPS are typically implemented using a double-barrelled style, where each function receives both a success continuation kk (for normal returns) and an exception continuation hh (for error cases), often written as f(x,k,h)f(x, k, h). For instance, a raise e operation evaluates ee to a value vv and invokes h(v)h(v), bypassing the success path, while a try e1 catch x => e2 installs a local handler by evaluating e1e1 with an updated hh' that binds xx to the raised value and proceeds to e2e2. This dual-continuation passing simulates try-catch semantics without altering the underlying CPS transformation, ensuring exceptions propagate to the appropriate handler.

scheme

;; 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

Challenges in this approach include ensuring that all computational paths—normal and exceptional—correctly invoke the intended , which requires meticulous propagation of both kk and hh through recursive calls and compositions to avoid errors. In full undelimited CPS, continuation leaks can occur if captured continuations are invoked multiple times or out of context, potentially leading to non-termination or incorrect state; delimited continuations mitigate this by bounding the scope of control transfers.

Applications

In functional programming

Continuation-passing style (CPS) plays a central role in the of languages, where it provides a mathematical framework for interpreting and related constructs by explicitly passing as arguments to functions. This approach, pioneered in early works on semantics, models as a series of transformations on continuations, enabling precise definitions of order and 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 where computation types are defined as CA=KARC_A = K_A \to R, with KAK_A as the continuation type for value type AA and RR as the response type. 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 is explicit, aiding in tasks like strictness analysis and code generation for evaluation. This transformation ensures that function applications become tail calls in the CPS form, which is crucial for efficient recursive functional code. CPS forms the foundational basis for monads in , particularly in handling effects like I/O and state through continuation-based structures. The monad in , defined as newtype 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 . 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. In , CPS enables the implementation of non-deterministic constructs like the amb operator for search, such as in parsers or , by reifying to capture and resume alternative computation paths. The amb operator, which introduces ambiguous choice, can be realized using first-class to implement failure and : 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 paradigms, integrates seamlessly with CPS interpreters for efficient evaluation of ambiguous expressions in functional contexts.

In concurrency and other domains

Continuation-passing style (CPS) has been extended to concurrency models by transforming threaded code into event-driven forms that unify native and cooperative threads. In Continuation-Passing C (CPC), a language extension for C, CPS converts imperative code to manage lightweight threads as heap-allocated continuations, enabling cooperative multitasking with yields that facilitate efficient context switches in the range of 16-46 nanoseconds on x86-64 architectures. This approach supports up to 50 million simultaneous threads with minimal overhead of about 82 bytes per continuation, as demonstrated in applications like the Hekate BitTorrent seeder. Delimited continuations further enhance safe concurrency in operating systems by capturing user process contexts during system calls, such as I/O operations, allowing suspension and resumption without race conditions through techniques like copy-on-write updates in structures like the Zipper File System. In , CPS models and choice points without relying on explicit stacks, transforming clauses into binary forms where each subgoal receives a continuation for subsequent goals. The BinWAM, a simplified for binary , stores these on a copy stack managed by garbage collection, eliminating traditional environment stack instructions and reducing choice point overhead. This continuation-passing model, as in BinProlog, maps full to binary logic programs, enabling implicit via failure continuations while maintaining efficiency in search exploration. Beyond these areas, CPS appears in for , where the Hakaru system transforms recursive probabilistic models into pure functions via CPS to enable and exact . In Hakaru's Mappl , CPS handles branching and calls by passing continuations that partition computations into dependent and independent parts, allowing summation over discrete variables with log-sum-exp operations to recover polynomial-time algorithms like the forward algorithm for hidden Markov models. In computer graphics, CPS supports ray tracing by lowering shaders to continuation form, pausing execution after ray queries and resuming via a stack-based that manages callbacks across shader stages in a big loop with switch statements. Modern applications include JavaScript's asynchronous programming, where callback-based patterns inherently follow CPS, passing continuations to handle non-blocking operations like data loading, with async/await providing over this style to simplify . In , recent semantics reconstruct core instructions in tail-recursive CPS to facilitate compositional analysis and optimizations, such as partial evaluation, by representing control structures like blocks and calls as a stack of continuations in a big-step interpreter.

References

Add your contribution
Related Hubs
User Avatar
No comments yet.