Abort
and call/cc
are non-local control operators.
Few programming languages provide operators as powerful as
call/cc
. But many languages do provide facilities that can be
built using non-local control operators.
The uses we have seen so far of call/cc
that have been
"downward" uses: only used to discard context. One mechanism found
in Ada and ML that uses downward continuations is exceptions. A simple
exception mechanism provides
(handle e h)
to establish a handler with dynamic extent;
(raise exp)
to raise an exception.
(extend-syntax (handle) ((handle e h) (with ((k gensym)) (let/cc k (fluid-let ((raise (lambda (x) (k (lambda () (h x)))))) (let ((v e)) (lambda () v))))))) (define raise (lambda (v) (error "unhandled exception" v)))We can define an I/O controller using this exception mechanism.
(define io-controller ... (handle (uncompress read) (lambda (exn) (case exn ('eof ...) ('else (raise exn)))))) (define read ... (if (eof-of-file) (raise 'eof)) ... ) (define uncompress (lambda (read) ... ))Here
io-controller
and read
are related procedures that share data structures
and information - in particular, they share an exception.
The uncompress
knows nothing about the
exception that read
can raise, or even that
read
can raise an exception at all.
Why do exception handlers have dynamic extent? Consider the following.
(handle (f (Pi (read))) (lambda (exn) ... handle 'not-a-number ...))If exceptions handlers were lexically scoped, like
catch
and
throw
, Pi
would have to appear within the above
procedure or be passed to the handler.
The next example shows that our implementation for dynamic-let
needs some additional work to act correctly in the presence
of let/cc
.
Why?
(define x 1) (let/cc k (fluid-let ((x 2)) (k 0)))
(define abort #f) (let/cc k (set! abort k))Now
abort
holds a continuation that behaves
like the abort operation
we discussed earlier. So (+ 1 (abort 'stopped))
returns
'stopped
. Suppose we now do
(define resume error) (+ 1 (let/cc k (begin (set! resume k) (abort 'stopped))))
resume
nows holds the continuation that knows how to resume
the interrupted computation.
(resume 0) => 1 (resume 1) => 2We can even run this multiple times. We can build a simple breakpointing facility this way.
(define break (lambda () (let/cc k (begin (set! resume k) (abort 'stopped-at-breakpoint)))))A breakpoint throws out the entire computation, but remembers how to get back. This is a simple upwards use of continuations. Here is another.
(define resume (lambda (co v) (let/cc k (co (cons v k))))) (define reader (lambda (writer) ((let* ((v1 (read)) (v2 (read)) (v3 (read))) (if (or (eof-object? v1) (eof-object? v2) (eof-object? v3)) 'done (let* ((writer (resume (cdr writer) v1)) (writer (resume (cdr writer) v2)) (writer (resume (cdr writer) v3))) (reader writer))))))) (define writer (lambda (reader) (let* ((v1 (car reader)) (reader (resume (cdr reader) #f)) (v2 (car reader))) (display (cons v1 v2)) (writer (resume (cdr reader) #f)))) (reader (resume writer #f))Observe that the reader knows nothing about how many values the writer needs at once, not does the writer know how many values the reader produces at once.