196: Delimited Continuations
Difficulty: โญโญโญโญโญ Level: Expert Capture "the rest of the computation up to a delimiter" as a reusable function โ the mind-bending abstraction that underlies async/await, generators, and nondeterministic search.The Problem This Solves
Normal exceptions let you jump up the call stack and throw away everything below the catch point. That's useful, but sometimes you want to jump up and come back. Imagine a generator: `yield 5` pauses execution, hands `5` to the caller, and when the caller asks for more, execution resumes from exactly that point. Or nondeterminism: `choose [1, 2, 3]` runs "the rest of the computation" three times, once for each choice. Both of these require capturing "the continuation" โ the rest of the program from the current point โ as a callable function. Delimited continuations are that mechanism. `reset` marks a boundary (the "delimiter"). `shift` captures everything between the current point and the nearest `reset` as a function `k`, then passes `k` to a user-provided function that can call `k` zero, one, or many times. Call it zero times: abort (like an exception). Call it once: transform (like a monad). Call it many times: nondeterminism, generators, backtracking. `async/await` in every language is implemented with delimited continuations internally. `await` is `shift`: it captures the continuation (rest of the async function), passes it to the runtime, and the runtime calls it when the future resolves. Generators (`yield`) work the same way. Delimited continuations are the unified foundation.The Intuition
Imagine a branching choose-your-own-adventure book. `reset` is the spine of the book. `shift` (via `choose`) is a page that says "go to page 42 if you chose A, go to page 97 if you chose B." Each branch continues reading the book from the same point. The "continuation" `k` is like a photocopier: it copies the rest of the book from the current page, so you can hand a copy to each branch. Both branches continue from the same place, independently. In code, `choose [1, 2, 3]` captures `k` = "everything that comes after the choose call up to the `reset`." It calls `k(1)`, `k(2)`, `k(3)`, and collects all results. This is exactly the list monad โ which is why the list monad and delimited continuations produce identical behavior for nondeterminism.How It Works in Rust
// Approach 1: List monad โ direct translation, most idiomatic
fn nd_bind<A, B, F: Fn(A) -> Vec<B>>(xs: Vec<A>, f: F) -> Vec<B> {
xs.into_iter().flat_map(f).collect() // flat_map IS the nondeterminism handler
}
fn choose<A: Clone>(xs: Vec<A>) -> Vec<A> { xs } // "perform shift"
fn nd_guard(cond: bool) -> Vec<()> { if cond { vec![()] } else { vec![] } }
// Find Pythagorean triples โ each choose runs "the rest" for every value:
let triples = nd_bind(choose((1..=10).collect()), |a| {
nd_bind(choose((a..=10).collect()), move |b| {
nd_bind(choose((b..=10).collect()), move |c| {
nd_bind(nd_guard(a*a + b*b == c*c), move |_| vec![(a,b,c)])
})
})
});
// [(3,4,5), (6,8,10)] โ all solutions, automatically explored
// Approach 2: CPS (continuation-passing style) shift/reset
// Closer to the formal definition; useful for understanding the semantics.
use std::rc::Rc;
type Cont<A, R> = Rc<dyn Fn(A) -> R>;
type DelimComp<A, R> = Rc<dyn Fn(Cont<A, R>) -> R>;
// reset(m): run m with the identity continuation (no transformation)
fn dc_reset<A: Clone + 'static>(m: DelimComp<A, A>) -> A {
m(Rc::new(|x| x)) // "return yourself" = the delimiter
}
// shift(g): g receives the current continuation k
fn dc_shift<A: 'static, R: 'static, G: Fn(Cont<A, R>) -> R + 'static>(g: G) -> DelimComp<A, R> {
Rc::new(move |k: Cont<A, R>| g(k)) // hand k to g; g decides what to do with it
}
// Nondeterminism via CPS: choose_cps calls k once per value, collects results
fn choose_cps<A: Clone + 'static>(xs: Vec<A>) -> DelimComp<A, Vec<A>> {
Rc::new(move |k: Cont<A, Vec<A>>| {
xs.iter().flat_map(|x| k(x.clone())).collect()
// ^^^^^^^^^^^^ run continuation with each choice
})
}
// The deep insight: choose_cps IS dc_shift with g = |k| xs.iter().flat_map(k).collect()
// And nd_reset_cps IS dc_reset with answer type Vec<A>
// The list monad and CPS shift/reset compute the same thing โ proven here.
fn nd_reset_cps<A: Clone + 'static>(m: DelimComp<A, Vec<A>>) -> Vec<A> {
m(Rc::new(|x| vec![x])) // base case: wrap single result in vec
}
let results = nd_reset_cps(dc_bind(
choose_cps(vec![1_i32, 2, 3]),
Rc::new(|x| dc_bind(
choose_cps(vec![10_i32, 20]),
Rc::new(move |y| nd_return_cps(x + y)),
)),
));
// [11, 21, 12, 22, 13, 23] โ all sums
// Approach 3: Permutations โ classic nondeterminism application
fn perms(list: Vec<i32>) -> Vec<Vec<i32>> {
if list.is_empty() { return vec![vec![]]; }
nd_bind(list.clone(), |x| {
let rest: Vec<i32> = list.iter().filter(|&&y| y != x).cloned().collect();
nd_bind(perms(rest), move |mut tail| {
tail.insert(0, x);
vec![tail]
})
})
}
// perms(vec![1,2,3]) โ 6 permutations
What This Unlocks
- Understanding async/await โ `await` is `shift`: capture the rest of the async function as a continuation, register it with the runtime, resume when the future is ready. Every `async` function compiles to a state machine encoding exactly this.
- Prolog-style backtracking โ constraint solvers, type unification, regex matching, SAT solvers all use nondeterminism with pruning via `nd_guard`; delimited continuations are the clean semantic model.
- Effect handler implementation โ OCaml 5's effect system is implemented using delimited continuations under the hood. Understanding shift/reset explains why `continue k value` works the way it does.
Key Differences
| Concept | OCaml | Rust | ||||
|---|---|---|---|---|---|---|
| reset | `match f () with \ | v -> v \ | effect (Shift k) handler -> handler k` | `dc_reset(m)` = `m(Rc::new(\ | x\ | x))` in CPS; or implicit in list monad |
| shift | `shift f = perform (Shift f)` โ captures continuation natively | `dc_shift(g)` = `Rc::new(\ | k\ | g(k))` in CPS; `choose` = `flat_map` in list monad | ||
| Resuming `k` multiple times | Native โ OCaml continuations are heap-allocated, can be called N times | `Rc<dyn Fn(A) -> R>` (cloneable, multi-use); `Box<dyn FnOnce>` can't be cloned | ||||
| Nondeterminism equivalent | `choose xs = shift (fun k -> List.concat_map k xs)` | `choose(xs) = xs; nd_bind = flat_map` โ list monad is the Rust idiom | ||||
| Use in production | OCaml 5 effects and continuations are production-ready | CPS is academic; use iterators/rayon for real nondeterminism; use tokio for continuations |
// Delimited Continuations via Simulation
//
// OCaml 5 effects give `shift` and `reset` natively.
// Rust has neither, but we can simulate the classic use case:
// nondeterminism (list-of-successes / Prolog-style search).
//
// `reset` marks the delimiter; `shift` captures the continuation up to
// the nearest `reset` and passes it to a function that decides what to do.
//
// The standard translation: shift/reset for nondeterminism โ
the list monad.
// We implement:
// 1. A direct list-monad simulation (most idiomatic, same semantics)
// 2. A CPS-based shift/reset using Rc to handle multiple invocations
use std::rc::Rc;
// โโ 1. List monad (direct translation of shift/reset nondeterminism) โโโโโโ
fn nd_return<A: Clone>(x: A) -> Vec<A> {
vec![x]
}
fn nd_bind<A, B, F: Fn(A) -> Vec<B>>(xs: Vec<A>, f: F) -> Vec<B> {
xs.into_iter().flat_map(f).collect()
}
/// "choose" = shift in the OCaml example
fn choose<A: Clone>(xs: Vec<A>) -> Vec<A> {
xs
}
/// "guard" โ zero or one result based on a predicate
fn nd_guard(cond: bool) -> Vec<()> {
if cond { vec![()] } else { vec![] }
}
// โโ 2. CPS-based shift/reset โโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโ
//
// We use Rc so continuations can be invoked multiple times (needed for
// nondeterminism where the continuation runs once per branch).
//
// type DelimComp<A, R> = Rc<dyn Fn(Rc<dyn Fn(A) -> R>) -> R>
// reset(m) = m(Rc::new(|x| x))
// shift(g) = Rc::new(move |k| g(k))
type Cont<A, R> = Rc<dyn Fn(A) -> R>;
type DelimComp<A, R> = Rc<dyn Fn(Cont<A, R>) -> R>;
fn dc_return<A: Clone + 'static, R: 'static>(x: A) -> DelimComp<A, R> {
Rc::new(move |k: Cont<A, R>| k(x.clone()))
}
fn dc_bind<A: Clone + 'static, B: Clone + 'static, R: Clone + 'static>(
m: DelimComp<A, R>,
f: Rc<dyn Fn(A) -> DelimComp<B, R>>,
) -> DelimComp<B, R> {
Rc::new(move |k: Cont<B, R>| {
let f2 = f.clone();
let k2 = k.clone();
m(Rc::new(move |a: A| {
let mb = f2(a);
mb(k2.clone())
}))
})
}
fn dc_reset<A: Clone + 'static>(m: DelimComp<A, A>) -> A {
m(Rc::new(|x| x))
}
/// shift: g receives the current continuation
fn dc_shift<A: 'static, R: 'static, G>(g: G) -> DelimComp<A, R>
where
G: Fn(Cont<A, R>) -> R + 'static,
{
Rc::new(move |k: Cont<A, R>| g(k))
}
// โโ 3. Nondeterminism via CPS shift/reset โโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโ
fn choose_cps<A: Clone + 'static>(xs: Vec<A>) -> DelimComp<A, Vec<A>> {
Rc::new(move |k: Cont<A, Vec<A>>| {
xs.iter().flat_map(|x| k(x.clone())).collect()
})
}
fn nd_return_cps<A: Clone + 'static>(x: A) -> DelimComp<A, Vec<A>> {
dc_return(x)
}
fn nd_reset_cps<A: Clone + 'static>(m: DelimComp<A, Vec<A>>) -> Vec<A> {
m(Rc::new(|x| vec![x]))
}
fn main() {
// โโ List monad (direct) โโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโ
println!("=== List monad nondeterminism ===");
// Choose x from [1..3], y from [x..x+2], return (x,y) if x+y<=5
let results = nd_bind(choose(vec![1, 2, 3]), |x| {
nd_bind(choose(vec![x, x + 1, x + 2]), move |y| {
nd_bind(nd_guard(x + y <= 5), move |_| nd_return((x, y)))
})
});
println!("x in [1..3], y in [x..x+2], x+y<=5: {:?}", results);
// Pythagorean triples up to 10
let triples = nd_bind(choose((1..=10).collect()), |a| {
nd_bind(choose((a..=10).collect()), move |b| {
nd_bind(choose((b..=10).collect()), move |c| {
nd_bind(nd_guard(a * a + b * b == c * c), move |_| nd_return((a, b, c)))
})
})
});
println!("Pythagorean triples โค10: {:?}", triples);
println!();
// โโ CPS shift/reset nondeterminism โโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโ
println!("=== CPS shift/reset nondeterminism ===");
let cps_results = nd_reset_cps(dc_bind(
choose_cps(vec![1_i32, 2, 3]),
Rc::new(|x| {
dc_bind(
choose_cps(vec![10_i32, 20]),
Rc::new(move |y| nd_return_cps(x + y)),
)
}),
));
println!("[1,2,3] ร [10,20] sums: {:?}", cps_results);
println!();
// โโ Classic "all permutations" via list monad โโโโโโโโโโโโโโโโโโโโโ
println!("=== Permutations of [1,2,3] ===");
fn remove_one(x: i32, list: &[i32]) -> Vec<i32> {
list.iter().filter(|&&y| y != x).cloned().collect()
}
fn perms(list: Vec<i32>) -> Vec<Vec<i32>> {
if list.is_empty() {
return vec![vec![]];
}
nd_bind(list.clone(), |x| {
let rest = remove_one(x, &list);
nd_bind(perms(rest), move |mut tail| {
tail.insert(0, x);
nd_return(tail)
})
})
}
let ps = perms(vec![1, 2, 3]);
println!("permutations: {} total", ps.len());
for p in &ps {
println!(" {:?}", p);
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_list_monad_combinations() {
let results = nd_bind(choose(vec![1, 2]), |x| {
nd_bind(choose(vec![10, 20]), move |y| nd_return(x + y))
});
assert_eq!(results.len(), 4);
assert!(results.contains(&11));
assert!(results.contains(&22));
}
#[test]
fn test_pythagorean_triple_3_4_5() {
let triples = nd_bind(choose((1..=10).collect()), |a| {
nd_bind(choose((a..=10).collect()), move |b| {
nd_bind(choose((b..=10).collect()), move |c| {
nd_bind(nd_guard(a * a + b * b == c * c), move |_| nd_return((a, b, c)))
})
})
});
assert!(triples.contains(&(3, 4, 5)));
assert!(triples.contains(&(6, 8, 10)));
}
#[test]
fn test_cps_shift_reset_sums() {
let cps_results = nd_reset_cps(dc_bind(
choose_cps(vec![1_i32, 2, 3]),
Rc::new(|x| {
dc_bind(
choose_cps(vec![10_i32, 20]),
Rc::new(move |y| nd_return_cps(x + y)),
)
}),
));
assert_eq!(cps_results.len(), 6);
assert!(cps_results.contains(&11));
assert!(cps_results.contains(&23));
}
#[test]
fn test_guard_filters_with_empty() {
let results = nd_bind(choose(vec![1, 2, 3, 4, 5]), |x| {
nd_bind(nd_guard(x % 2 == 0), move |_| nd_return(x))
});
assert_eq!(results, vec![2, 4]);
}
}
(* Delimited continuations: capture only part of the call stack.
OCaml 5 effects give us delimited continuations naturally. *)
effect Shift : ('a -> 'b) -> 'a
let reset f =
match f () with
| v -> v
| effect (Shift k) handler -> handler k
let shift f = perform (Shift f)
(* Classic: list of successes (nondeterminism) *)
let choose lst =
shift (fun k -> List.concat_map k lst)
let () =
(* Pythagorean triples up to 10 *)
let triples =
reset (fun () ->
let a = choose [1;2;3;4;5;6;7;8;9;10] in
let b = choose [a;a+1;a+2;a+3;a+4;a+5;a+6;a+7;a+8;a+9;a+10] in
let c = choose [b;b+1;b+2;b+3;b+4;b+5] in
if a*a + b*b = c*c then [(a,b,c)] else [])
in
List.iter (fun (a,b,c) -> Printf.printf "(%d,%d,%d)\n" a b c) (List.filteri (fun i _ -> i < 5) triples)