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
readare related procedures that share data structures and information - in particular, they share an exception. The
uncompressknows nothing about the exception that
readcan raise, or even that
readcan 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
Piwould have to appear within the above procedure or be passed to the handler.
The next example shows that our implementation for
needs some additional work to act correctly in the presence
(define x 1) (let/cc k (fluid-let ((x 2)) (k 0)))
(define abort #f) (let/cc k (set! abort k))Now
abortholds 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))))
resumenows 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.