You are on page 1of 8

Latest: HOWTO: Get tenure

Next: First-class macros from meta-circular evaluators


Prev: Compiling Scheme to C
Rand: Boost productivity: Cripple your technology

Continuations by example:
Exceptions, time-traveling
search, generators, threads, and
coroutines
[article index] [email me] [@mattmight] [+mattmight] [rss]

Continuations are the least understood of all control-flow constructs. This


lack of understanding (or awareness) is unfortunate, given that
continuations permit the programmer to implement powerful language
features and algorithms, including exceptions, backtracking search, threads,
generators and coroutines.

I think part of the problem with continuations is that they're always


explained with quasi-metaphysical phrases: "time travel," "parallel
universes," "the future of the computation." I wrote this article so that my
advanced compilers students could piece together how continuations
worked by example.

Ladrillos Decool 498 Unids Thor Marvel Super


heroes Hulk Figuras de Acción Bu… €27,80
ALIEXPRESS

More resources
If you like this, you might also like:

My recommended reading for programming languages.


Compiling with Continuations.
MIT's Structure and Interpretation of Computer Programs.

Teasers: Omniscience, SAT-solving


Using first-class continuations, it is possible to add a "non-deterministic (or
ambiguous) choice" procedure to a language: amb. The amb procedure takes a
list of values, and chooses one of them. The catch here is that amb must
choose a value so that all assert statements encountered in the future of the
computation are true. For example, the following Scheme code:
(let ((a (amb (list 1 2 3 4 5 6 7)))
(b (amb (list 1 2 3 4 5 6 7)))
(c (amb (list 1 2 3 4 5 6 7))))

; We're looking for dimensions of a legal right


; triangle using the Pythagorean theorem:
(assert (= (* c c) (+ (* a a) (* b b))))

; And, we want the second side to be the shorter one:


(assert (< b a))

; Print out the answer:


(display (list a b c))
(newline))

prints out (4 3 5). The amb procedure chose these values so that when the
assertions were encountered later on, they were true.

With amb available, it's trivial to write a SAT-solving macro, so that:


(sat-solve (and (implies a (not b))
(not a)
c)
(list a b c))

evaluates to (#f #f #t). That is, the macro (sat-solve formula body) binds
the free variables in the formula so that the formula evaluates to true, and
then it evaluates the body expression in the context of these satisfying
assigments.

What are continuations?


Concretely, a continuation is a procedure that represents the remaining
steps in a computation. For the expression 3 * (f() + 8), think about the
remaining steps in the compuation after evaluating the expression f(). For
example, in C/Java, the procedure current_continuation is the continuation
of the call to f():
void current_continuation(int result) {
result += 8 ;
result *= 3 ;
(continuation of 3 * (f() + 8))(result) ;
}

The value passed to the continuation is the return value of the call.

Some languages (like Scheme and SML/NJ) provide a way to capture the
current continuation as a first-class value. In Scheme, the procedure
call-with-current-continuation (often abbreviated call/cc) takes a
procedure, and passes it the current continuation of the computation.

The standard idiom for call/cc has an explicit lambda term as its argument:
(call/cc (lambda (current-continuation)
body))

During the execution of the expression body, the variable current-


continuation is bound to the current continuation. If invoked, current-
continuation immediately returns from the call to call/cc, and call/cc
returns whatever value was passed to current-continuation.
For example, the code:
(display
(call/cc (lambda (cc)
(display "I got here.\n")
(cc "This string was passed to the continuation.\n")
(display "But not here.\n"))))

will display:
I got here.
This string was passed to the continuation.

What makes first-class continuations so powerful is that the continuation


may still be called even after the call to call/cc is finished. For example, the
following causes an infinite loop that prints Going to invoke (start)
forever:
(let ((start #f))

(if (not start)


(call/cc (lambda (cc)
(set! start cc))))

(display "Going to invoke (start)\n")

(start))

An idiom for continuation programming


By itself, call/cc can do anything you might need to do with continuations,
but it is not the most convenient API. When programming with
continuations, I recommend using the a helper procedure, current-
continuation:

(define (current-continuation)
(call/cc (lambda (cc) (cc cc))))

The procedure current-continuation returns the continuation in which it


was evaluated.

Then, you can use a conditional pattern to detect whether the continuation
was just created, or the continuation has been invoked from some later
point:
(let ((cc (current-continuation)))
(cond
((procedure? cc) body)
((future-value? cc) handling-body)
(else (error "Contract violation!"))))

The user-defined predicate future-value? should be able to identify the


value passed to the continuation. (Some Scheme implementations support
continuation?, which should be used instead of procedure? if it's available.)

Implementing go-when and right-now


An alternative interface for interacting with continuations consists of two
procedures: go-when and right-now:

(right-now) returns the current moment in the computation; and


(go-when moment) returns the computation to a previously captured
moment.

Some applications of continuation may be naturally expressed with just


these two procedures.

[gowhen.scm]

#lang racket

; right-now : -> moment


(define (right-now)
(call-with-current-continuation
(lambda (cc)
(cc cc))))

; go-when : moment -> ...


(define (go-when then)
(then then))

; An infinite loop:
(let ((the-beginning (right-now)))
(display "Hello, world!")
(newline)
(go-when the-beginning))

Time-traveling search
Now we can expose how it was possible to write the teaser examples in the
introduction. Under the hood, the procedures amb and assert communicate
with each other through a "fail stack." The top of the fail stack contains the
next continuation to be invoked whenever an assertion fails. When an
assertion fails, it invokes the procedure fail, which pops the fail stack and
invokes the result. The amb procedure pushes a new continuation on top of
the fail stack, selects the first value in the list, and then removes it. If the list
is empty, then the procedure amb invokes the procedure fail.

[amb.scm]

#lang racket

; current-continuation : -> continuation


(define (current-continuation)
(call-with-current-continuation
(lambda (cc)
(cc cc))))

; fail-stack : list[continuation]
(define fail-stack '())

; fail : -> ...


(define (fail)
(if (not (pair? fail-stack))
(error "back-tracking stack exhausted!")
(begin
(let ((back-track-point (car fail-stack)))
(set! fail-stack (cdr fail-stack))
(back-track-point back-track-point)))))
; amb : list[a] -> a
(define (amb choices)
(let ((cc (current-continuation)))
(cond
((null? choices) (fail))
((pair? choices) (let ((choice (car choices)))
(set! choices (cdr choices))
(set! fail-stack (cons cc fail-stack))

Exceptions
Exceptions are easily implemented with continations. When an exception is
thrown, control needs to return to the first enclosing try form.

To create this behavior with continuations, try forms capture the current
continuation before evaluating their body. In Scheme, an implementation of
try will also use dynamic-wind to maintain a stack of exception handlers.

When evaluating (dynamic-wind before-thunk do-thunk after-thunk), the


run-time will ensure that before-thunk is called whenever execution enters
the "dynamic extent" of the computation within do-thunk, and after-thunk
is called whenever execution leaves the "dynamic extent" of the
computation.

The try block can exploit this behavior by using the before-thunk to push
the exception handler, and the after-thunk to pop the exception handler.

[exceptions.scm]

; current-continuation : -> continuation


(define (current-continuation)
(call-with-current-continuation
(lambda (cc)
(cc cc))))

; exception-stack : list[continuation]
(define exception-stack '())

; (try exp ... catch catch-procedure) runs


; exp ..., and invokes catch-procedure with
; the value passed to throw.
(define-syntax try
(syntax-rules (catch)
((_ exp ... catch proc)
; =>
(let ((cc (current-continuation)))
(cond
((procedure? cc)
(dynamic-wind
(lambda ()
(set! exception-stack (cons cc exception-stack)))
(lambda ()
exp ...)
(lambda ()
(set! exception-stack (cdr exception-stack)))))

((pair? cc)

Generators
Generators are an elegant solution to the problem of iterating over complex
data structures like trees and graphs. Imagine you want to write a for loop
like the following:
for (node n in tree t) {
do something with n
}

Some languages (like Java) allow custom iterators, so you could write
something like:
for (Node n : t) {
do something with n
}

But, how do you write the iterator t? You could do a walk over the tree and
turn it into an array or a list, but this is inefficient--you allocate more than
you need to, and you essentially end up iterating twice. The efficient solution
(in a language like Java) is to write an iterator that remembers where it is in
the search of the tree. This is difficult to get right, and it doesn't feel natural.
(Try writing it!)

A simple tree walk is easier to write. If the language supports first-class


continuations, then you can use the tree walk approach to iterate without
the overhead of an intermediate data structure.

A generator is a procedure that accepts a yield procedure as an argument,


with the expectation that it will iterate over some data structure and call
yield on every element within the data structure. With continuations, we can
instrument a generator to make it suitable for overheadless for-style
iteration.

The key idea is to toggle between two continuations--one in the loop, and
the other in the generator. When the loop needs another value, it will switch
to the continuation inside the generator (passing it a continuation for the
loop), and once the generator has a value, it will pass a value back (plus a
new continuation for the generator).

[generators.scm]

; current-continuation : -> continuation


(define (current-continuation)
(call-with-current-continuation
(lambda (cc)
(cc cc))))

; void : -> void


(define (void)
(if #f #t))

; tree-iterator : tree -> generator


(define (tree-iterator tree)
(lambda (yield)

;; Walk the tree, yielding the leaves.

(define (walk tree)


(if (not (pair? tree))
(yield tree)
(begin
(walk (car tree))
(walk (cdr tree)))))

(walk tree)))

; make-yield : continuation -> (value -> ...)


(define (make-yield for-cc)
(lambda (value)

Cooperative threads (coroutines)


Continuations make it straightforward to implement cooperative multi-
threading (and with macros, it is not difficult to simulate preemptive
multithreading.)

In cooperative multithreading, a thread must yield control manually; it will


not be preemptively switched out.

The API for cooperative multithreading has five functions:

(spawn thunk) puts a thread for thunk into the thread queue.
(quit) kills the current thread and removes it from the thread queue.
(yield) hands control from the current thread to another thread.
(start-threads) starts executing threads in the thread queue.
(halt) exits all threads.

[coop-threads.scm]

; thread-queue : list[continuation]
(define thread-queue '())

; halt : continuation
(define halt #f)

; void : -> void


(define (void) (if #f #t))

; current-continuation : -> continuation


(define (current-continuation)
(call-with-current-continuation
(lambda (cc)
(cc cc))))

; spawn : (-> anything) -> void


(define (spawn thunk)
(let ((cc (current-continuation)))
(if (procedure? cc)
(set! thread-queue (append thread-queue (list cc)))
(begin (thunk)
(quit)))))

; yield : value -> void


(define (yield)
(let ((cc (current-continuation)))
(if (and (procedure? cc) (pair? thread-queue))
(let ((next-thread (car thread-queue)))

[article index] [email me] [@mattmight] [+mattmight] [rss]

Latest: HOWTO: Get tenure


Next: First-class macros from meta-circular evaluators
Prev: Compiling Scheme to C
Rand: Boost productivity: Cripple your technology
matt.might.net is powered by linode | legal information

You might also like