next up previous
Next: Continuations in The Interpreter Up: Part II, due Friday, Previous: Part II, due Friday,

Continuation Passing

Start by considering our normal Scheme evaluator. The situation at any point during evaluation of an expression can usefully be understood as consisting of two parts: rules for finding a value of a subexpression, and rules for what to do when the value is found. It is possible to rewrite a procedure so the rules for what to do with the returned value are packaged into an explicit continuation procedure. Instead of returning the subexpression value, the computation ``passes on'' the value by applying the continuation procedure to the value.

Because values of subexpressions are always passed on, they never get returned, except perhaps at a final step. And since no value gets returned after a procedure call, continuation style procedures always turn out to be tail-recursive. We explained early in the term that tail-recursive procedures provide a stack-space saving benefit. That's important, but not our main concern.

As we indicated, the real point of having explicit continuations is to manage flow of control during computation. This can be a valuable technique in Scheme programming regardless of whether or not we are defining an interpreter. The added benefit of writing an interpreter in continuation passing style is that it becomes easy to add all sorts of flow of control primitives to the language being implemented by the interpreter.

As a first example of how explicit continuations work, we'll consider the recursive procedure flatten. Applying flatten to a tree returns a list of its leaves. For example, evaluation of

(flatten '((a (b) ((c (d))) e)))
returns the list (a b c d e).

The usual way to define flatten is by a tree recursive procedure:

(define (flatten tree)
  (cond ((null? tree) '())
        ((not (pair? tree)) (list tree))
        (else (append (flatten (car tree)) 
                      (flatten (cdr tree))))))

It isn't obvious how such a procedure could be made tail recursive. Here's how: we will write a continuation style flatten procedure cs-flatten that takes two arguments, a tree, T, to be flattened and a one argument continuation procedure, receiver.

Evaluating (flatten T receiver) leads to receiver being applied to the flattened list of leaves of T. For example,

(define identity (lambda (l) l))
;Value: "identity --> #[compound-procedure 17 identity]"

(cs-flatten '(((a) b) (((c)))) identity)
;Value: (a b c)

(cs-flatten '(((a) b) (((c)))) second)
;Value: b

(define (announce-null leaves)
  (if (null? leaves)
      (begin (display "empty tree") 
             nil)
      leaves))
;Value: "announce-null --> #[compound-procedure 18 announce-null]"

(cs-flatten '(((a) b) (((c)))) announce-null)
;Value: (a b c)

(cs-flatten '() announce-null)
empty tree
;Value: #f

To define cs-flatten, we begin with the observation that the usual recursion for flattening a tree, T, consists of three main steps:

  1. flatten the car of T,
  2. flatten the cdr of T,
  3. append the results of these two flattenings.

Let's assume for the moment that we can take append as a primitive. We can accomplish the first step, flattening the car of T, by calling (cs-flatten (car T) identity). But this call returns the list of leaves instead of passing it on. Continuation passing strategy is to pass the leaves of the car to a continuation that will go on to perform the remaining two steps.

Now the second step is to flatten the cdr of T, so a continuation to accomplish the second step is

(lambda (flattened-car) (cs-flatten (cdr T) ...)
What goes in the ellipsis (three dots) above? The answer is a continuation that receives the leaves of the cdr and performs the third step of appending the two lists. This final list must then be passed on to receiver. So this final continuation is:
(lambda (cdr-leaves) (receiver (append car-leaves cdr-leaves)))
Here is the final definition:
(define (cs-flatten T receiver)
  (cond ((pair? T)
         (cs-flatten                    ;(1.) flatten
          (car T)                       ;the car of T, and pass the result to
          (lambda (car-leaves)          ;a procedure that will
            (cs-flatten                 ;(2.) flatten
             (cdr T)                    ;the cdr of T, and pass the result to
             (lambda (cdr-leaves)       ;a procedure that will
                                ;(3.) append the two flattened lists
                                ;and pass the resulting list to RECEIVER:
               (receiver
                (append car-leaves cdr-leaves)))))))
        ((null? T) (receiver nil))
        (else (receiver (list T)))))

It may seem like cheating to treat append as primitive. But we can also define append in continuation passing style: break the usual recursion for appending two lists into two steps:

  1. append the cdr of the first list to the second list, and
  2. cons the car of the first list onto the resulting list.
This leads to
(define (cs-append l1 l2 list-receiver)
    (if (pair? l1)
        (cs-append               ;(1.) append
         (cdr l1) l2             ;the CDR of l1 to l2, and pass the result to
         (lambda (appended-cdr)  ;a procedure that will
                             ;(2.) CONS the CAR of l1 to that result,
                             ;and pass the final list to LIST-RECEIVER:
           (list-receiver
            (cons (car l1) appended-cdr))))
        (list-receiver l2)))

Putting cs-append together with cs-flatten yields an entirely tail-recursive, continuation passing procedure:

(define (cs-flatten T receiver)
  (define (cs-append l1 l2 list-receiver)
    (if (pair? l1)
        (cs-append
         (cdr l1) l2
         (lambda (appended-cdr)
           (list-receiver
            (cons (car l1) appended-cdr))))
        (list-receiver l2)))
  (cond ((pair? T)
         (cs-flatten
          (car T)
          (lambda (car-leaves)
            (cs-flatten
             (cdr T)
             (lambda (cdr-leaves)
               (cs-append car-leaves cdr-leaves receiver)))))) ;NOTE THE CHANGE HERE
        ((null? T) (receiver nil))
        (else (receiver (list T)))))

(define (flatten T)
   (cs-flatten T identity))

Problem 10: Define a tail-recursive, continuation passing style procedure matchup-var-vals taking three arguments: a list of variables (represented as Scheme symbols), a corresponding list of values, and a receiver procedure. Applying matchup-var-vals will split the list of variables into two lists--those that do not satisfy a dynamic? predicate and those that do--and likewise split the corresponding list of values into two lists. These four lists will be passed as arguments to the receiver procedure.

The dynamic? predicate is included in c-eval.scm; it is satisfied precisely by symbols that start with %d-. For example,

(matchup-var-vals
 '(aa dd %d-xx %d %dd %d-aa)
 '(1  2  3     4  5   6)
 list)
;Value: ((aa dd %d %dd) (1 2 4 5) (%d-xx %d-aa) (3 6))

You may assume that the first two arguments to matchup-var-vals are of equal length.

Also, remember that you are defining this in the normal Scheme evaluator.

Hint: think about how many arguments the continuation procedure should take.


next up previous
Next: Continuations in The Interpreter Up: Part II, due Friday, Previous: Part II, due Friday,
W. Eric L. Grimson 2002-11-14