1 Definition and Semantics
First‑class continuations are a control flow abstraction that allows a program to capture its current execution state—including the call stack, program counter, and pending computations—as a first‑class object. This object, called a continuation, can be stored, passed as an argument, returned from functions, and later invoked to resume execution from the exact point of capture. The term “first‑class” means continuations have the same rights as any other value in the language: they can be created, assigned to variables, and used as arguments or return values.
1.1 The concept of a continuation
A continuation represents the “rest of the computation” from a given point in a program. In a purely sequential language, the evaluation of an expression implicitly has a continuation: once the expression is evaluated, the interpreter proceeds with the remaining code. Making this continuation explicit and manipulable gives the programmer fine‑grained control over the flow of execution.
1.1.1 Representation in denotational semantics
In denotational semantics, continuations are central to modeling control flow. The meaning of a program is expressed as a function that maps initial states to final answers, with the continuation playing a key role in sequencing.
1.1.1.1 Formal definition of a continuation as a function from an expression’s result to the final answer
Formally, a continuation is a function that takes the result of an expression and returns the final answer of the entire program. For an expression *e* whose evaluation yields a value *v*, the continuation *κ* is applied: *κ*(*v*) yields the final result. Under this view, every expression is interpreted in the context of a continuation that specifies what to do with its value. The denotation of an expression is a function that takes a continuation and returns the final answer.
1.2 First‑class vs. second‑class continuations
Second‑class continuations cannot be stored or passed around arbitrarily; they exist only implicitly during program execution (e.g., the implicit continuation used by exceptions). First‑class continuations are full citizens of the language: they can be named, stored in data structures, and invoked at any time. This distinction is analogous to that between first‑class and second‑class functions. Most languages with continuations provide only first‑class forms, though some offer both (e.g., Scheme’s call/cc is first‑class, while exception‑handling mechanisms are second‑class).
1.3 Capturing a continuation
Capturing a continuation means obtaining a representation of the current execution state. The captured object can later be used to “jump back” to that point, restoring the entire call stack and deferred computations.
1.3.1 The call‑with‑current‑continuation (call/cc) primitive
call/cc (short for “call‑with‑current‑continuation”) is the classic mechanism for capturing first‑class continuations, introduced in Scheme. It takes a function *f* as an argument and passes the current continuation to *f*. The continuation is itself a function: when invoked with a value, that value is returned to the point where call/cc was originally called.
1.3.1.1 Example: simple continuation capture and invocation in Scheme
(define (test)
(call/cc
(lambda (k)
;; k is the current continuation
(+ 1 (k 42))))) ; (k 42) aborts the addition and returns 42 from call/cc
When test is called, (k 42) invokes the continuation, causing call/cc to yield 42 directly, ignoring the pending addition of 1. Thus the result is 42, not 43.
1.3.2 Alternative capture mechanisms (e.g., reset/shift for delimited continuations)
Delimited continuations, captured via operators like reset and shift, restrict the captured continuation to a specific “prompt” rather than the entire call stack. This makes them composable and avoids the global control flow inversion of call/cc. The shift operator captures the continuation up to the nearest enclosing reset boundary, and the captured continuation can be used as a function within a delimited context.
2 Use Cases and Applications
First‑class continuations enable the direct implementation of many advanced control structures without special language support. They are particularly useful in situations that require non‑local jumps, backtracking, or cooperative scheduling.
2.1 Implementing coroutines and generators
Coroutines are subroutines that can be suspended and resumed. With continuations, each suspension point captures the current state; resuming invokes the captured continuation. Generators, a restricted form of coroutines that yield sequences of values, can be built similarly.
2.1.1 Producer‑consumer patterns with continuations
A producer generates values on demand, while a consumer processes them. Continuations allow the producer to suspend after yielding a value and the consumer to resume the producer for the next value. This pattern avoids the need for explicit buffers or threads.
2.1.1.1 Example: cooperative multitasking using call/cc
In Scheme, one can implement a simple task scheduler: each task calls call/cc to yield control, passing its continuation to the scheduler. The scheduler holds a queue of continuations and resumes them in turn. This yields cooperative multitasking without separate threads.
(define (task n)
(let loop ((i 0))
(when (< i n)
(display i) (newline)
;; yield to scheduler
(call/cc (lambda (k) (scheduler k)))
(loop (+ i 1)))))
2.2 Exception handling and non‑local exits
Continuations provide a natural mechanism for non‑local exits: a continuation captured before a risky operation can be invoked to abort that operation and transfer control to a handler. In languages without built‑in exceptions, call/cc can implement try‑catch semantics handily.
2.3 Backtracking and search algorithms
Backtracking algorithms, such as those used in constraint satisfaction, explore multiple solution paths. When a dead end is reached, the algorithm can fall back to a previous state by invoking a continuation saved earlier. This avoids manual state management.
2.3.1 Solving constraint satisfaction problems
For example, a Sudoku solver can use continuations to try a value; if a conflict arises, it invokes the continuation saved before the attempt to backtrack. The continuation captures the entire state, so undoing is automatic.
2.4 Cooperative multitasking and event loops
In single‑threaded environments, continuations allow an event loop to simulate concurrency. Each event handler can capture its continuation before returning control to the loop; the loop later resumes it when the event is ready. This pattern underlies many early cooperative multitasking systems.
3 Implementation Considerations
Implementing first‑class continuations affects the runtime system significantly, particularly in how the call stack is managed and how continuations interact with memory management.
3.1 Continuation representation in runtime systems
At the runtime level, a continuation must capture the entire execution state: the call stack contents, program counter, local variables, and any pending computations. Different strategies exist for representing this state.
3.1.1 Stack‑based vs. heap‑allocated continuations
In a stack‑based representation, the continuation keeps a reference to the current stack segment. This is efficient when continuations are used linearly (e.g., exceptions) but fails for first‑class use because the stack may be shared. Heap‑allocated continuations copy the stack (or a portion of it) into the heap, allowing arbitrary lifetimes and multiple invocations. Languages like Scheme often use a hybrid approach, allocating continuations on the heap only when they are captured as first‑class objects.
3.1.2 Copying, capturing, and reentrancy semantics
Capturing a continuation may require copying the relevant stack frames to prevent mutation by later code. If a captured continuation is invoked multiple times, the runtime must handle reentrancy correctly: each invocation restores the state exactly as captured. Some implementations treat continuations as immutable snapshots; others permit destructive updates, leading to subtle semantics.
3.2 Performance implications and overhead
First‑class continuations impose overhead because capturing a continuation is more expensive than a function call: it involves copying stack data and updating garbage‑collector roots. Invoking a continuation also requires restoring a potentially large state. Programs that use continuations heavily (e.g., for backtracking) may suffer performance degradation. Many modern languages avoid them for this reason, preferring restricted alternatives like generators.
3.3 Interaction with garbage collection and memory management
Captured continuations can keep large portions of the stack alive, potentially increasing memory pressure. Garbage collectors must treat continuation objects as roots, scanning the captured frames for references. If continuations are used sparingly, this overhead is negligible; however, in long‑running programs that capture many continuations, memory leaks can occur if continuations are not released.
4 Relationship to Other Control Mechanisms
First‑class continuations are a foundational abstraction; many other control structures can be derived from them or compared to them.
4.1 Continuation‑passing style (CPS)
Continuation‑passing style is a programming style where every function takes an explicit continuation argument, rather than returning normally. This makes control flow entirely explicit and eliminates the need for a built‑in stack. Many compilers use CPS as an intermediate representation.
4.1.1 Transforming a direct‑style program into CPS
In CPS transformation, each function call becomes a tail call to a continuation. For example, the expression f(x) + 1 becomes f(x, λv. (+ v 1)), where the continuation receives the result and continues. The resulting program has no nested control stack, making it well‑suited for manipulation by continuations.
4.2 Delimited continuations (composable, prompt‑based)
Delimited continuations, as captured by reset/shift or control/prompt, restrict the continuation to a programmable boundary. Unlike call/cc, which captures the entire stack, delimited continuations are composable and have a clear return type. They are used in languages like Scala (via the scala.util.continuations plugin) and in Haskell (Cont monad with reset/shift).
4.3 Coroutines and fibers in modern languages (e.g., Lua, C++20)
Modern languages often provide coroutines or fibers as more controlled alternatives to first‑class continuations. Lua’s coroutines are symmetric, resumable subroutines that do not expose an arbitrary continuation; C++20 introduced std::coroutine_handle for lightweight user‑level threads. These restrict the capture to specific yield points, improving performance and safety.
4.4 Resumable exceptions and yield constructs
Some languages (e.g., Python with generators, JavaScript with yield) offer resumable exceptions or generator functions that implicitly capture continuations at yield points. These are a form of limited first‑class continuations, as the captured state is more structured and cannot be invoked outside the generator’s context.
5 History and Language Support
The development of first‑class continuations is closely tied to the history of functional programming and Lisp dialects.
5.1 Origins in lambda calculus and early Lisp
The concept of continuations has roots in denotational semantics and the lambda calculus, where control flow is modeled by continuations. Early Lisp systems (late 1950s) did not have first‑class continuations, but they influenced later work.
5.1.1 The development of call/cc in Scheme (mid‑1970s)
Scheme, designed by Gerald Jay Sussman and Guy L. Steele in the mid‑1970s, introduced call/cc as a primitive control operator. It was inspired by the catch and throw mechanisms of earlier Lisps but elevated to a fully first‑class object. call/cc appeared in the revised⁴ report on Scheme (R⁴RS) and became a landmark feature.
5.2 Languages with first‑class continuations
Several programming languages have provided first‑class continuations, though adoption has been limited.
5.2.1 Scheme (full support via call/cc and dynamic-wind)
Scheme remains the canonical language for first‑class continuations. The dynamic-wind primitive is used together with call/cc to protect resource acquisition and release across continuation invocations.
5.2.2 Standard ML of New Jersey (via callcc and throw)
Standard ML of New Jersey (SML/NJ) provides callcc (call‑with‑current‑continuation) and throw for capturing and invoking continuations. It also includes a calleecc for delimited forms. SML/NJ’s implementation is known for its efficiency and use in compiler research.
5.2.3 Ruby’s deprecated callcc
Ruby historically included callcc (as Kernel#callcc), but it was deprecated in Ruby 2.0 and removed in later versions due to performance and complexity issues.
5.3 Influence on modern languages (e.g., Scala’s delimited continuations, C#’s async/await)
The ideas of first‑class continuations influenced later language features. Scala’s delimited continuations (via the shift and reset macros) and C#’s async/await (which is compiled into a state machine resembling continuation passing) are direct descendants. Python’s generators and JavaScript’s async functions also borrow the concept of suspension and resumption.
6 Controversies and Debates
First‑class continuations have been both praised for their expressive power and criticized for their complexity.
6.1 Readability and maintainability concerns
Because continuations allow control to jump arbitrarily, programs that use them heavily can become difficult to read and debug. The flow of control is not apparent from the lexical structure; a captured continuation may be invoked from any context, making reasoning about program state challenging.
6.1.1 The “goto” of control flow abstractions
Critics have likened first‑class continuations to a structured form of goto, in that they can create “spaghetti control flow.” Just as unstructured jumps were replaced by structured control, some argue that continuations should be used sparingly in favor of more structured abstractions (like exceptions or generators).
6.2 Potential for misuse and spaghetti control flow
Overuse of continuations can lead to code that is hard to refactor, test, or prove correct. The unrestricted capture of the entire stack means that even innocent changes elsewhere in the program may affect the behavior of a continuation. This fragility has discouraged widespread adoption in production systems.
6.3 Adoption in production versus academic interest
Despite their theoretical elegance, first‑class continuations are rarely used in mainstream production code. Most languages that implement them do so for research or prototyping. The overhead, complexity, and difficulty of optimization have led to a preference for restricted forms (e.g., generators, async/await). Nonetheless, they remain an important tool in programming language theory and serve as a benchmark for understanding control flow.