Evaluation Context
1 Overview
2 API Reference
absurd
label
goto
current-continuation
cc
wait-for-future-continuation
wait/  fc
return-with-current-continuation
return/  cc
return-with-values
2.1 Typed Racket Definitions
¬
Label
LEM
3 Examples
3.1 Loop
3.2 Early Return
3.3 Light-Weight Processes
3.4 Ambiguous Operator
3.5 Yin-Yang Puzzle
3.6 Defining call/  cc
3.7 Context-Frozen Thunks
3.7.1 Freezing parameterize Bindings
3.7.2 Freezing Exception Handlers
3.7.3 Freezing dynamic-wind Guards
3.7.4 Composing Frozen Contexts
9.1

Evaluation Context🔗ℹ

1 Overview🔗ℹ

This package provides label, goto, and cc (short for current-continuation), which are simpler, more direct alternatives to call/cc.

The key idea is that continuations can be understood through three equivalent lenses, each with different trade-offs:

  • First-class labels (label + goto) — conceptually the simplest, with a trivial type (Label), but requires mutation (set!) to communicate across jumps.

  • Law of Excluded Middle + Law of Noncontradiction (cc) — eliminates the need for mutation by returning a union type (LEM a) = a(¬ a), at the cost of branching on whether you received a value or a continuation.

  • Peirce’s law (call/cc) — eliminates both mutation and union types by delivering the continuation as an argument to a callback, at the cost of a more complex higher-order type signature.

All three are equivalent in expressive power. This package provides the first two as building blocks, and shows how call/cc can be defined in terms of them (and vice versa).

Additionally, the package provides three operators for constructing context-frozen thunks—values of type (¬ (¬ a)) that freeze the current evaluation context and can later be triggered via call/cc as double negation elimination:

  • wait/fc is the core primitive. It captures the current evaluation context and hands a future continuation—one that does not yet exist at capture time—to a user-supplied function proc. Whatever proc returns is delivered to that future continuation. When proc itself has type (¬ (¬ a)), wait/fc has type ( (¬ (¬ a)) (¬ (¬ a))), making frozen contexts directly composable.

  • return/cc is wait/fc specialized to plain thunks: it ignores the future continuation and simply evaluates thk in the captured context, delivering the result automatically.

  • return-with-values is the trivial, purely functional Double Negation Introduction (DNI): it wraps values of type a into a function of type (¬ (¬ a)) without capturing any evaluation context. No continuations, no side effects.

2 API Reference🔗ℹ

procedure

(absurd)  any

The Principle of Explosion (ex falso quodlibet): from falsehood, anything follows.

It is a function that matches no arguments and therefore cannot be invoked:

(: absurd ( (b) (  b)))
(define absurd (case-λ))

procedure

(label [prompt-tag])  any/c

  prompt-tag : continuation-prompt-tag?
   = (default-continuation-prompt-tag)
Captures the current position in the program and returns a Label value. A subsequent (goto l) jumps back to this point, causing label to "return again".

The optional prompt-tag argument specifies which continuation prompt to capture up to, defaulting to (default-continuation-prompt-tag).

Implementation using call/cc:

(: label (→* () (Prompt-TagTop) Label))
(define (label [prompt-tag (default-continuation-prompt-tag)])
  (call/cc goto prompt-tag))

Implementation using cc:

(: label (→* () (Prompt-TagTop) Label))
(define (label [prompt-tag (default-continuation-prompt-tag)])
  (cc prompt-tag))

procedure

(goto k [v])  none/c

  k : (-> any/c none/c)
  v : any/c = k
Jumps to the label k, passing v as the value delivered at the jump target. If v is omitted, it defaults to k itself—this is consistent with the interpretation that goto is itself a Label, since Label = (¬ Label).

Equivalent definitions:

(: goto ( (a) (case→ ( Label ) ( (¬ a) a ))))
(define (goto k [v k]) (k v))

Using cc:

(: goto ( (a) (case→ ( Label ) ( (¬ a) a ))))
(define (goto k [v k]) (cc k v))

procedure

(current-continuation [prompt-tag])  any

  prompt-tag : continuation-prompt-tag?
   = (default-continuation-prompt-tag)
(current-continuation k v ...)  none/c
  k : (-> any/c ... none/c)
  v : any/c
The core operator of this package, combining the Law of Excluded Middle (LEM) and the Law of Noncontradiction (LNC) into a single procedure.

Zero arguments or prompt tag (LEM): Captures the current continuation and returns it as a function. The first time (current-continuation) is evaluated, it returns a continuation of type (¬ a) — a function that, when called with a value of type a, jumps back to this point, causing (current-continuation) to "return again" with that value. Thus the overall return type is ( a (¬ a)).

Continuation and values (LNC): Invokes the continuation k with the given values v .... This never returns to the caller (return type ).

The optional prompt-tag argument specifies which continuation prompt to capture up to, defaulting to (default-continuation-prompt-tag).

Implementation using call/cc:

(: current-continuation ( (a) (case→ (→* () (Prompt-TagTop) ( a (¬ a))) ( (¬ a) a ))))
(define current-continuation
  (case-λ
    [() (current-continuation (default-continuation-prompt-tag))]
    [(p)
     (if (continuation-prompt-tag? p)
         (call-with-current-continuation values p)
         (p))]
    [(k . v*) (apply k v*)]))

Implementation using label and goto:

(: current-continuation ( (a) (case→ (→* () (Prompt-TagTop) ( a (¬ a))) ( (¬ a) a ))))
(define current-continuation
  (case-λ
    [() (current-continuation (default-continuation-prompt-tag))]
    [(p)
     (if (continuation-prompt-tag? p)
         (let* ([v* #f] [l (label p)])
           (if v*
               (apply values v*)
               (λ vs (set! v* vs) (goto l))))
         (p))]
    [(k . v*) (apply k v*)]))

procedure

(cc [prompt-tag])  any

  prompt-tag : continuation-prompt-tag?
   = (default-continuation-prompt-tag)
(cc k v ...)  none/c
  k : (-> any/c ... none/c)
  v : any/c
An alias for current-continuation.

procedure

(wait-for-future-continuation proc 
  [prompt-tag]) 
  (-> (-> any/c ... none/c) none/c)
  proc : (-> (-> any/c ... none/c) any)
  prompt-tag : continuation-prompt-tag?
   = (default-continuation-prompt-tag)
The core operator for constructing context-frozen thunks.

Takes a function proc of type ( (¬ a) a) and captures the current evaluation context, returning a context-frozen thunk of type (¬ (¬ a)).

When the returned context-frozen thunk is invoked with a continuation k (typically via call/cc as double negation elimination), control jumps back to the evaluation context where wait/fc was originally called. There, proc is called with k as the future continuation—a continuation that did not yet exist when wait/fc was evaluated. Whatever proc returns is delivered to k via call-with-values.

This means the computation in proc runs with the original exception handlers, dynamic-wind guards, parameterize bindings, and other context-sensitive state, while still reflecting the current variable environment.

When proc always invokes k (i.e., proc has type (¬ (¬ a))), the type of wait/fc becomes ( (¬ (¬ a)) (¬ (¬ a))), making frozen contexts directly composable: the output of one wait/fc call can serve as the proc argument of another.

The optional prompt-tag argument specifies which continuation prompt to capture up to, defaulting to (default-continuation-prompt-tag).

Implementation using cc:

(: wait/fc ( (a) (→* (( (¬ a) a)) (Prompt-TagTop) (¬ (¬ a)))))
(define (wait/fc proc [prompt-tag (default-continuation-prompt-tag)])
  (define first? #t)
  (define k (cc prompt-tag))
  (unless first? (call-with-values (λ () (proc k)) k))
  (set! first? #f)
  k)

procedure

(wait/fc proc [prompt-tag])  (-> (-> any/c ... none/c) none/c)

  proc : (-> (-> any/c ... none/c) any)
  prompt-tag : continuation-prompt-tag?
   = (default-continuation-prompt-tag)

procedure

(return-with-current-continuation thk 
  [prompt-tag]) 
  (-> (-> any/c ... none/c) none/c)
  thk : (-> any)
  prompt-tag : continuation-prompt-tag?
   = (default-continuation-prompt-tag)
Takes a thunk of type ( a) and returns a context-frozen thunk—a continuation of type (¬ (¬ a)) that captures the current evaluation context.

return/cc is wait/fc specialized to the case where the body is a plain thunk thk. The future continuation is ignored. For cases where the body needs access to the future continuation—for example, to compose frozen contexts—use wait/fc directly.

The optional prompt-tag argument specifies which continuation prompt to capture up to, defaulting to (default-continuation-prompt-tag).

Implementation using wait/fc:

(: return/cc ( (a) (→* (( a)) (Prompt-TagTop) (¬ (¬ a)))))
(define (return/cc thk [prompt-tag (default-continuation-prompt-tag)])
  (wait/fc (λ (_) (thk)) prompt-tag))

procedure

(return/cc thk [prompt-tag])  (-> (-> any/c ... none/c) none/c)

  thk : (-> any)
  prompt-tag : continuation-prompt-tag?
   = (default-continuation-prompt-tag)

procedure

(return-with-values v ...)  (-> (-> any/c ... none/c) none/c)

  v : any/c
The purely functional Double Negation Introduction (DNI).

Takes any number of values and returns a context-frozen thunk of type (¬ (¬ a)) that, when invoked with a continuation k,simply delivers those values to k. No continuation is captured; no evaluation context is frozen.

(: return-with-values ( (a) ( a (¬ (¬ a)))))
(define (return-with-values . v*) (λ (k) (apply k v*)))

This is the logical fact that from a we can always derive (¬ (¬ a)): given a value, we can always construct a context-frozen thunk that produces it, trivially, with no context to freeze. Compare with wait/fc and return/cc, which do capture an evaluation context.

2.1 Typed Racket Definitions🔗ℹ

The following type definitions are provided for use with Typed Racket. They formalize the correspondence between control operators and classical logic.

type

The empty type, representing a computation that never produces a value (i.e., diverges or transfers control elsewhere). An alias for Nothing.

(define-type  Nothing)

type constructor

(¬ a)

The negation of type a: a function that consumes an a and never returns. In the Curry–Howard correspondence, this is logical negation.

(define-type (¬ a) ( a ))

type

Label

The type of first-class labels. Defined as the fixed point of ¬: a label is a function that accepts a label and never returns.

(define-type Label (¬ Label))

This recursive type reflects the fact that goto itself is a Label.

type constructor

(LEM a)

The Law of Excluded Middle as a type: for any a, you either have a value of type a or a continuation of type (¬ a). This is the return type of (cc) when called with zero arguments.

(define-type (LEM a) ( a (¬ a)))

3 Examples🔗ℹ

Each example is shown in multiple styles—using call/cc, cc, and label/goto—to illustrate the trade-offs between the three approaches.

3.1 Loop🔗ℹ

A simple counted loop using a first-class label:

(let ([x 0])
  (define loop (label))
  (set! x (add1 x))
  (when (< x 7) (goto loop))
  (displayln x))

3.2 Early Return🔗ℹ

Short-circuiting a product computation when a zero is encountered.

Using call/cc:

(define (mul . r*)
  (call/cc
   (λ (return)
     (for/fold ([res (*)] #:result (return res))
               ([r (in-list r*)])
       (if (zero? r)
           (return r)
           (* res r))))))

Using cc:

(define (mul . r*)
  (define result (cc))
  (when (continuation? result)
    (for/fold ([res (*)] #:result (cc result res))
              ([r (in-list r*)])
      (if (zero? r)
          (cc result r)
          (* res r))))
  result)

Using label and goto:

(define (mul . r*)
  (define first? #t)
  (define result (*))
  (define l (label))
  (when first?
    (set! first? #f)
    (for ([r (in-list r*)])
      (when (zero? r)
        (set! result r)
        (goto l))
      (set! result (* result r))))
  result)

3.3 Light-Weight Processes🔗ℹ

A simple cooperative multitasking scheduler. Multiple "threads" yield control with pause and are round-robin scheduled through a queue.

Using call/cc:

(let ([lwp-queue (make-queue)])
  (define (lwp thk)
    (enqueue! lwp-queue thk))
  (define (start)
    (when (non-empty-queue? lwp-queue)
      ((dequeue! lwp-queue))))
  (define (pause)
    (call/cc
     (λ (k)
       (enqueue! lwp-queue (λ () (k #f)))
       (start))))
 
  (lwp (λ () (let f () (pause) (display #\h) (f))))
  (lwp (λ () (let f () (pause) (display #\e) (f))))
  (lwp (λ () (let f () (pause) (display #\y) (f))))
  (lwp (λ () (let f () (pause) (display #\!) (f))))
  (lwp (λ () (let f () (pause) (newline)     (f))))
  (start))

Using cc:

(let ([lwp-queue (make-queue)])
  (define (lwp thk)
    (enqueue! lwp-queue thk))
  (define (start)
    (when (non-empty-queue? lwp-queue)
      ((dequeue! lwp-queue))))
  (define (pause)
    (define k (cc))
    (when k
      (enqueue! lwp-queue (λ () (cc k #f)))
      (start)))
 
  (lwp (λ () (let f () (pause) (display #\h) (f))))
  (lwp (λ () (let f () (pause) (display #\e) (f))))
  (lwp (λ () (let f () (pause) (display #\y) (f))))
  (lwp (λ () (let f () (pause) (display #\!) (f))))
  (lwp (λ () (let f () (pause) (newline)     (f))))
  (start))

Using label and goto:

(let ([lwp-queue (make-queue)])
  (define (lwp thk)
    (enqueue! lwp-queue thk))
  (define (start)
    (when (non-empty-queue? lwp-queue)
      ((dequeue! lwp-queue))))
  (define (pause)
    (define first? #t)
    (define l (label))
    (when first?
      (set! first? #f)
      (enqueue! lwp-queue (λ () (goto l)))
      (start)))
 
  (lwp (λ () (let f () (pause) (display #\h) (f))))
  (lwp (λ () (let f () (pause) (display #\e) (f))))
  (lwp (λ () (let f () (pause) (display #\y) (f))))
  (lwp (λ () (let f () (pause) (display #\!) (f))))
  (lwp (λ () (let f () (pause) (newline)     (f))))
  (start))

3.4 Ambiguous Operator🔗ℹ

McCarthy’s amb operator for nondeterministic programming via backtracking. The operator explores alternatives depth-first and backtracks on failure.

Using call/cc:

(let ([task* '()])
  (define (fail)
    (if (null? task*)
        (error "Amb tree exhausted")
        ((car task*))))
  (define (amb* . alt*)
    (call/cc
     (λ (task)
       (unless (null? alt*)
         (set! task* (cons task task*)))))
    (when (null? alt*) (fail))
    (define alt (car alt*))
    (set! alt* (cdr alt*))
    (when (null? alt*) (set! task* (cdr task*)))
    (alt))
  (define-syntax-rule (amb exp* ...) (amb* (λ () exp*) ...))
 
  (let ([w-1 (amb "the" "that" "a")]
        [w-2 (amb "frog" "elephant" "thing")]
        [w-3 (amb "walked" "treaded" "grows")]
        [w-4 (amb "slowly" "quickly")])
    (define (joins? left right)
      (equal?
       (string-ref left (sub1 (string-length left)))
       (string-ref right 0)))
    (unless (joins? w-1 w-2) (amb))
    (unless (joins? w-2 w-3) (amb))
    (unless (joins? w-3 w-4) (amb))
    (list w-1 w-2 w-3 w-4)))

Using cc:

(let ([task* '()])
  (define (fail)
    (if (null? task*)
        (error "Amb tree exhausted")
        (cc (car task*) #f)))
  (define (amb* . alt*)
    (define task (cc))
    (when (null? alt*) (fail))
    (when task
      (set! task* (cons task task*)))
    (define alt (car alt*))
    (set! alt* (cdr alt*))
    (when (null? alt*) (set! task* (cdr task*)))
    (alt))
  (define-syntax-rule (amb exp* ...) (amb* (λ () exp*) ...))
 
  (let ([w-1 (amb "the" "that" "a")]
        [w-2 (amb "frog" "elephant" "thing")]
        [w-3 (amb "walked" "treaded" "grows")]
        [w-4 (amb "slowly" "quickly")])
    (define (joins? left right)
      (equal?
       (string-ref left (sub1 (string-length left)))
       (string-ref right 0)))
    (unless (joins? w-1 w-2) (amb))
    (unless (joins? w-2 w-3) (amb))
    (unless (joins? w-3 w-4) (amb))
    (list w-1 w-2 w-3 w-4)))

Using label and goto:

(let ([task* '()])
  (define (fail)
    (if (null? task*)
        (error "Amb tree exhausted")
        (goto (car task*))))
  (define (amb* . alt*)
    (define first? #t)
    (define task (label))
    (when (null? alt*) (fail))
    (when first?
      (set! first? #f)
      (set! task* (cons task task*)))
    (define alt (car alt*))
    (set! alt* (cdr alt*))
    (when (null? alt*) (set! task* (cdr task*)))
    (alt))
  (define-syntax-rule (amb exp* ...) (amb* (λ () exp*) ...))
 
  (let ([w-1 (amb "the" "that" "a")]
        [w-2 (amb "frog" "elephant" "thing")]
        [w-3 (amb "walked" "treaded" "grows")]
        [w-4 (amb "slowly" "quickly")])
    (define (joins? left right)
      (equal?
       (string-ref left (sub1 (string-length left)))
       (string-ref right 0)))
    (unless (joins? w-1 w-2) (amb))
    (unless (joins? w-2 w-3) (amb))
    (unless (joins? w-3 w-4) (amb))
    (list w-1 w-2 w-3 w-4)))

All three produce '("that" "thing" "grows" "slowly").

3.5 Yin-Yang Puzzle🔗ℹ

David Madore’s famous puzzle, which exploits the fact that goto is itself a Label. Since Label = (¬ Label), any label can be passed to any other label.

(let ([yin (label)])
  (display #\@)
  (let ([yang (label)])
    (display #\*)
    (yin yang)))

Using call/cc:

(let ([kn (call/cc (λ (k) k))])
  (display #\@)
  (let ([kn+1 (call/cc (λ (k) k))])
    (display #\*)
    (kn kn+1)))

Using cc:

(let ([kn (cc)])
  (display #\@)
  (let ([kn+1 (cc)])
    (display #\*)
    (cc kn kn+1)))

Using label and goto:

(let* ([k #f] [k0 (label)])
  (unless k (set! k k0) (goto k))
  (display #\@)
  (let* ([kn k] [kn+1 (label)])
    (when (eq? kn k) (set! k kn+1) (goto k))
    (display #\*)
    (goto kn)))

CPS transform (no continuations at all):

(define (k0 kn)
  (display #\@)
  (define (kn+1 k)
    (display #\*)
    (kn k))
  (kn+1 kn+1))
(k0 k0)

3.6 Defining call/cc🔗ℹ

call/cc can be defined in terms of cc:

(define (call/cc proc [prompt-tag (default-continuation-prompt-tag)])
  (define v* (cc prompt-tag))
  (if (list? v*)
      (apply values v*)
      (proc (λ vs (cc v* vs)))))

And in terms of label and goto:

(define (call/cc proc [prompt-tag (default-continuation-prompt-tag)])
  (define v* #f)
  (define l (label prompt-tag))
  (if v*
      (apply values v*)
      (proc (λ vs (set! v* vs) (goto l)))))

3.7 Context-Frozen Thunks🔗ℹ

The wait/fc operator creates context-frozen thunks: computations that run in the evaluation context where they were defined, not where they are called. This freezes exception handlers, dynamic-wind guards, parameterize bindings, and other context-sensitive state, while ordinary mutable variables remain live. return/cc is the specialization of wait/fc to plain thunks. return-with-values is the degenerate case where no context is frozen at all.

3.7.1 Freezing parameterize Bindings🔗ℹ

A plain thunk evaluates in the caller’s parameter context. A context-frozen thunk always evaluates in its birth context, regardless of what parameterize bindings are active at the call site. With wait/fc, the future continuation is available to proc, which decides what value to return:

(define a (make-parameter 1))
(define b 111)
(define f (wait/fc (λ (_) (* (a) b))))
(displayln (call/cc f))
; => 111 (a=1, b=111)
(set! b 222)
(displayln (call/cc f))
; => 222 (a=1, b=222 — variable change is visible)
(parameterize ([a 2])
  (displayln (call/cc f)))
; => 222 (a=1, b=222 — parameterize at call site is invisible)

Contrast with a plain thunk, which would yield 444 in the last case because (a) would be 2. Also contrast with return-with-values, which captures no context at all:

(define g (return-with-values 42))
(parameterize ([a 99]) (displayln (call/cc g)))
; => 42 (no context frozen — the value was fixed at construction)

3.7.2 Freezing Exception Handlers🔗ℹ

The body of a context-frozen thunk runs under the exception handlers that were active at definition time, regardless of what handlers are active at the call site:

(define f
  (with-handlers ([exn:fail? (λ (e) (error "caught at birth"))])
    (wait/fc (λ (k) (error "boom")))))
 
; Calling from a context with a different handler:
(with-handlers ([exn:fail? (λ (e) (error "caught at call site"))])
  (displayln (call/cc f)))
; => "caught at birth" (call-site handler is bypassed)

3.7.3 Freezing dynamic-wind Guards🔗ℹ

Entry and exit guards from the definition context are re-entered when the frozen thunk is invoked:

(define f
  (dynamic-wind
    (λ () (displayln "[Entry] enter birth context"))
    (λ () (wait/fc (λ (k) (displayln "[Body] evaluate thunk"))))
    (λ () (displayln "[Exit] leave birth context"))))
 
(displayln "--- call ---")
(call/cc f)
(displayln "--- done ---")
; Output:
; [Entry] enter birth context
; [Exit] leave birth context
; --- call ---
; [Entry] enter birth context
; [Body] evaluate thunk
; [Exit] leave birth context
; --- done ---

3.7.4 Composing Frozen Contexts🔗ℹ

Since wait/fc exposes the future continuation to proc, frozen contexts can be composed: when proc has type (¬ (¬ a)), wait/fc has type (¬ (¬ a)) (¬ (¬ a)), so the output of one wait/fc can serve directly as the proc argument of another.

The future continuation passes through each frozen layer in sequence—like a relay baton—with each layer’s dynamic-wind guards firing as control enters and exits:

; Time 1: In context C1, create W1
(define W1
  (dynamic-wind
    (λ () (displayln "W1: Enter"))
    (λ () (wait/fc
           (λ (fc)
             (displayln "Reached the innermost context C1")
             (fc 'hello))))
    (λ () (displayln "W1: Exit"))))
 
; Time 2: In context C2, wrap W1 in another layer
(define W2
  (dynamic-wind
    (λ () (displayln "W2: Enter"))
    (λ () (wait/fc W1))
    (λ () (displayln "W2: Exit"))))
 
; Time 3: In context C3, trigger W2
(dynamic-wind
  (λ () (displayln "W3: Enter"))
  (λ () (displayln (call/cc W2)))
  (λ () (displayln "W3: Exit")))

Output:

; W1: Enter
; W1: Exit
; W2: Enter
; W2: Exit
; W3: Enter
; W3: Exit
; W2: Enter
; W2: Exit
; W1: Enter
; Reached the innermost context C1
; W1: Exit
; W3: Enter
; hello
; W3: Exit

When (call/cc W2) is invoked in C3, the future continuation is delivered first to C2’s frozen context, which forwards it to C1’s frozen context via W1. The innermost proc finally receives the continuation and calls (fc 'hello), sending 'hello all the way back to C3.