COS 441 - Continuing Upward - Mar 12, 1996

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

We can define these as follows.
(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
   (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)))

Continuing Upward

Suppose we do
(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
         (set! resume k)
         (abort 'stopped))))
resume nows holds the continuation that knows how to resume the interrupted computation.
(resume 0) => 1
(resume 1) => 2
We can even run this multiple times. We can build a simple breakpointing facility this way.
(define break
  (lambda ()
    (let/cc k
        (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.


Imagine a producer-consumer problem, where the producer has lots of complex state to maintain; so does the consumer, and they don't synchronize very well. For example, the producer makes 3 values available per loop, and the consumer consumes 2. Co-routines are a mechanism for decoupling unrelated computations and switching between them. A co-routine resumes another co-routine, rather than calling it. Resuming another, a co-routine suspends, so that it may in turn be resumed later.
(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))
	   (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.