Professional Documents
Culture Documents
Continuations by example:
Exceptions, time-traveling
search, generators, threads, and
coroutines
[article index] [email me] [@mattmight] [+mattmight] [rss]
More resources
If you like this, you might also like:
prints out (4 3 5). The amb procedure chose these values so that when the
assertions were encountered later on, they were true.
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.
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))
will display:
I got here.
This string was passed to the continuation.
(start))
(define (current-continuation)
(call/cc (lambda (cc) (cc cc))))
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!"))))
[gowhen.scm]
#lang racket
; 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
; fail-stack : list[continuation]
(define 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.
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]
; exception-stack : list[continuation]
(define 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!)
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]
(walk tree)))
(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)