An expression's continuation is "the computation that will receive the result of that expression". For example, in the expression
(+ 4 (+ 1 2))
the result of (+ 1 2)
will be added to 4. The
addition to 4 is that expression's continuation. If we wanted to
represent the continuation of (+ 1 2)
, we might
write:
(lambda (v) (+ 4 v))
That is, the continuation of (+ 1 2)
takes a
value, and adds four to that value.
Every expression has an implicit continuation. In most
languages, continuations are hidden under the covers and not
accessible at all. In Scheme, the current continuation can be
reified as a function by using the built-in function
call-with-current-continuation
, or
call/cc
for short.
(call/cc expr)
does the following:
C
that takes one
argument, and applies the current continuation with that
argument value.expr
--- i.e., it invokes (expr
C)
.(expr
C)
, unless expr
calls
C
, in which case the value that is passed to
C
is returned.Here is an example:
(+ 4 (call/cc (lambda (cont) (cont (+ 1 2)))))
This performs exactly the same computation as (+ 4 (+ 1
2))
. However, it uses call/cc
to capture the
current continuation, and then passes the result of evaluating
(+ 1 2)
directly to that continuation. Another,
roughly equivalent way of writing the above is as follows:
((lambda (cont) (cont (+ 1 2))) (lambda (v) (+ 4 v))
Continuations are first-class values, and so do not have to be called immediately:
> (define x ()) > (define (put-cont-in-x cont) (set! x cont)) > (define print-line (lambda (x) (display x) (newline))) >(print-line (call/cc put-cont-in-x)) #<void> > (x "hi!") "hi"
What's going on here? The current continuation at
(call/cc put-cont-in-x)
is the application of
print-line
. By the behavior of call/cc
,
put-cont-in-x
therefore receives a continuation that
takes its argument and passes it to print-line
.
put-cont-in-x
stores this continuation as the value
bound to x
. Finally, when we invoke x
,
the argument list gets passed to the continuation we captured ---
which passes its argument to print-line
, which then
prints the value followed by a newline.
A less trivial use of call/cc
follows. Consider a
function that searches a list a value satisfying some
predicate. The naive implementation is as follows:
(define naive-find (lambda (pred x) (cond ((null? x) ()) ((pred (car x)) (car x)) (else (naive-find pred (cdr x))))))
Suppose that Scheme did not perform proper tail calls. Then, this function would have to return from each recursive call when the recursion terminated. The following function fixes this problem by passing the final result directly to an earlier continuation:
(define find (lambda (pred aList) (call/cc (lambda (cont) (letrec ((helper (lambda (x) (cond ((null? x) (cont ())) ((pred (car x)) (cont (car x))) (else (helper pred (cdr x))))))) (helper pred aList))))))
This variation of find
uses call/cc
to bind the continuation to cont
. Then, when we
reach either terminating case ((null? x)
or
(pred (car x))
) of the recursion, we invoke
cont
with some value (()
or
x
). This skips all the recursive calls, and returns
the value directly to a continuation. We can prove this by
executing a slightly modified version of the naive and
continuation-calling find functions:
(define printing-naive-find (lambda (pred x) (cond ((null? x) ()) ((pred (car x)) (car x)) (else (let ((retval (printing-naive-find pred (cdr x)))) (display "returning from recursive call at: ") (display (car x)) (newline) retval))))) (define printing-find (lambda (pred x) (call/cc (lambda (cont) (letrec ((helper (lambda (pred x) (cond ((null? x) (cont ())) ((pred (car x)) (cont (car x))) (else (let ((retval (helper pred (cdr x)))) (display "returning from call at: ") (display (car x)) (newline) retval)))))) (helper pred x))))))
The printing-naive-find
prints values "on the way
back" from the last recursive call. The
printing-find
version does not, because the
continuation is captured before entering the recursive
function. The result is passed directly to the caller of
the outermost lambda
.
In the examples above, the naive-find
function is
tail-recursive. There's no need to use the continuation-invoking
optimization to pass the end result directly to caller of the
original invocation. However, for non-tail-recursive functions,
the use of continuations can make returning from the "last call" a
constant-time rather than linear-time operation.
The ability of call/cc
to break out of deeply
nested evaluation is often useful when returning errors. Consider
the following function:
(define divide-or-error (lambda (aList divisorList errorValue) (cond ((null? aList) ()) ((= (car divisorList) 0) errorValue) (else (let ((result (divide-or-error (cdr aList) (cdr divisorList) errorValue))) (if (= result errorValue) errorValue (cons (/ (car aList) (car divisorList)) result)))))))
This function divides each value in aList
by the
corresponding value in divisorList
, and returns a
list of the answers; however, if there is any zero value in
divisorList
, it returns errorValue
instead. Again, returning from each recursive call individually
is a waste of time when we only want to pass
errorValue
back to the original caller. By
capturing the initial continuation, we can return directly:
(define divide-or-error2 (lambda (aList divisorList errorValue) (call/cc (lambda (cont) (define helper (lambda (values divisors) (cond ((null? values) ()) ((= (car divisors) 0) (cont errorValue)) (else (cons (/ (car values) (car divisors)) (helper (cdr values) (cdr divisors))))))) (helper aList divisorList)))))
Notice that we no longer need to test the result of the
recursive call for errorValue
: when a zero is
detected in divisors
, it will be returned directly to
the caller of the outermost lambda
.
dynamic-wind
functionSometimes, we want to make sure that a function gets executed
regardless of whether a continuation "bypasses" part of the
currently evaluating expression. The dynamic-wind
function takes three arguments, each of which must be a
no-argument function value:
(dynamic-wind inFn bodyFn outFn)
The semantics of dynamic-wind
are as follows:
bodyFn
and return its value.bodyFn
is entered, either
during the current evaluation or throughe valuation of any
continuation captured inside bodyFn
,
inFn
will be called
beforehand.bodyFn
is exited, either during
the current evaluation or through evaluation of any continuation
captured inside bodyFn
,
outFn
will be called
afterwards.In the following example, open-input-file
opens an
input port (much like a file handle in other languages) which must
be closed afterwards with close-input-port
:
(define (safe-process input-file process-fn) (let ((p (open-input-file input-file))) (dynamic-wind (lambda () #f) (lambda () (process-fn p)) (lambda () (begin (display "closing input port...") (newline) (close-input-port p))))))
The in-function does nothing; the out-function closes the input
port; and the body function applies the process-fn
to
the opened input port. Regardless of how process-fn
exists, the out-function will always be called. (Typical usage of
the out-function parameter to dynamic-wind
closely
resemble that of finally
in Java.)
Now, let us apply safe-process
to a function that
exits prematurely:
;; Define a processing function that always exits to toplevel. (define exit-to-toplevel 'dummy) (call/cc (lambda (cont) (set! toplevel-exit cont))) (define (process p) (toplevel-exit)) ;; Observe that dynamic-wind still executes the code which ;; closes the input port. (safe-process "lecture14-dynamic-wind.ss" process) ; displays: "closing input port..."
In ML and most other languages, raising and handling of
exception are special constructs in the language. In Scheme, we
can define exceptions as an ordinary library, using continuations
and dynamic-wind
.
It is easiest to understand how to define exceptions backwards,
by going from use to definition. Here's how we'd like to
use the raise
function:
;; Function that raises an exception. (define (find-or-raise pred x) (cond ((null? x) (raise '(empty "No such element in list."))) ((pred (car x)) (car x)) (else (find-or-raise pred (cdr x)))))
Notice that we're representing the exception value as a list, where the first element of the list is a symbol naming the exception and the remaining values in the list are values to be carried with the exception.
Here's how we might like to use the handle
function:
(define gt0 (lambda x (> x 0)) (define find-result (handle 'empty (lambda () (find-or-raise gt0 '(-1 -2 -3 -4))) (lambda (anExn) -1)))
That is, we would like the arguments to be
(Notice that this definition of handle
only adds
one handler per expression. It's no fundamentally harder to have
a handle
that takes multiple handlers and tries them
in sequence, so for simplicity we'll stick with this.)
Taken together, the uses of raise
and
handle
above should give roughly the behavior given
by the ML definitions:
exception Empty of string; fun findOrRaise (pred, x) = if null x then raise Empty "No such element in list" else if pred (hd x) then x else findOrRaise pred (tl x) val gt0 = fn x => x > 0; val findResult = findOrRaise gt0 [-1, -2, -3, -4] handle Empty msg => -1
Now, we just need to figure out how to implement
raise
and handle
...
At any given point in evaluation, there is a stack of "active" handlers. When an exception is raised, we must pop handlers off the stack until we reach one that can handle the current exception. Implementing a global stack as a list is easy enough:
(define handler-stack ()) (define (push-handler handler) (set! handler-stack (cons handler handler-stack))) (define (pop-handler) (if (not (null? handler-stack)) (let ((top (car handler-stack))) (begin (set! handler-stack (cdr handler-stack)) top))))
We'll postpone the describing of the handlers themselves until
we see the handler
function.
When we get an unhandled exception, we must exit to top-level and print a message. In order to do this, we must capture a top-level continuation, which we'll invoke whenever we raise an exception with an empty handler stack:
(define exit-to-toplevel 'dummy) (call/cc (lambda (cont) (set! exit-to-toplevel cont)))
Notice that anyone who evaluates
(exit-to-toplevel)
will now escape to toplevel,
halting evaluation of any enclosing expression. (To prove this to
yourself, write a deeply-nested expression that applies
exit-to-toplevel
.)
raise
functionThe handler stack is a list. When an exception is
raise
d, there are two cases:
handle
expression.The trickiest part of this is case 2(a). Recall our sample use
of handle
for the find-or-raise
invocation above: when the handler for 'empty
is
invoked, we must return -1
to the whole
handle
expression's continuation (in this case, the
execution of the define
for
find-result
).
So, in order to define raise
, we must have the
following three things available in the handler:
handle
expression.We will therefore represent a handler as a three-element list
(exn-name handler-cont handler-fn)
. Assuming that
handle
pushed these things onto the stack,
raise
is defined as follows:
(define (raise anExn) (if (null? handler-stack) (begin (map display '("uncaught exception: " anExn)) (exit-to-toplevel)) ;; else (let* ((exn-name (car anExn)) (handler (pop-handler)) (handler-exn-name (car handler))) (if (eq? exn-name handler-exn-name) (let ((handler-cont (cadr handler)) (handler-fn (caddr handler))) (handler-cont (handler-fn anExn))) ;; else (raise anExn)))))
handle
functionFinally, we must define handle
. The
handle
function must take three arguments: an
exception name, a body function, and a handler function. It must
evaluate the body function, but during the evaluation of the body
we must be prepared to handle any exception raised in the handler
--- i.e., we must add a handler value of the form (exn-name
handler-cont handler-fn)
to the handler stack.
We implement this using call/cc
and
dynamic-wind
:
(define (handle exnName bodyFn handlerFn) (call/cc (lambda (cont) (dynamic-wind (lambda () (push-handler (list exnName cont handlerFn))) bodyFn (lambda () (pop-handler))))))
Notice that we use the out-function to pop the handler when
bodyFn
has completed execution.
display
statements at various points in the
code to see when they get executed, and what the contents of
handler-stack
are.(define find-result ...)
example above:handle
.(call/cc
(lambda (cont)
... inside handle
. Represent a
continuation as cell with an arrow pointing to the point in
the code where the continuation's argument will be
"sent".dynamic-wind
inside
handle
, but before execution of
bodyFn
.bodyFn
(which will be
find-or-raise
)raise
.(handler-fn anExn)
inside
raise
.