๐Ÿฆ€ Functional Rust

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

Key Differences

ConceptOCamlRust
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 timesNative โ€” 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 productionOCaml 5 effects and continuations are production-readyCPS 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)