Lecture 20,
Comp 311
In this lecture, we will make repeated use of three key transformations used in building software: representation independence, switching representations, and CPS.
We begin by CPSing our interpreter:
((var? M) (k (lookup M env))) ((app? M) (Eval (app-rator M) env (lambda (f) (Eval (app-rand M) env (lambda (a) (Apply f a k))))))We then make it representation independent,
((var? M) (Pop k (lookup M env))) ((app? M) (Eval (app-rator M) env (Push-app-f M env k)))where
(define Push-app-f
(lambda (M env k)
(lambda (f)
(Eval (app-rand M) env
(lambda (a)
(Apply f a k))))))
and
(define Pop
(lambda (k v)
(k v)))
Then in Push-app-f we rewrite the last procedure to
(lambda (a) (Push-app-a f k))with
(define Push-app-a
(lambda (f k)
(lambda (a)
(Apply f a k))))
Now in Push-app-f, which now looks like this,
(define Push-app-f
(lambda (M env k)
(lambda (f)
(Eval (app-rand M) env
(lambda (a)
(Push-app-a f k))))))
we replace the lambda with (list 'app-f M env k)
to get
(define Push-app-f
(lambda (M env k)
(list 'app-f M env k)))
and Push-app-a is modified similarly, so we assume that
all continuations have distinguishing tags.
make-closure and expressed closures in terms of that.
Since our continuations are quite similar to closures, why can't we
use make-closure to represent continuations as well?
Given these definitions, we can now write down the code for
Pop. Recall that Pop needs to examine the
tag at the front of the continuation to determine what to do.
(define Pop
(lambda (k v)
(if (list? k)
(case (car k)
((app-f) (let ((M (cadr k))
(env (caddr k))
(k (cadddr k)))
((lambda (f)
(Eval (app-rand M) env
(Push-app-a f k)))
v))))
(k v))))
(Note that we should be able to transform the continuations one at a
time. Hence, Pop includes both methods of invoking
continuations.) But we know that ((lambda ...) ...) is
just let, so we can rewrite the app-f case
as:
(let ((M (cadr k))
(env (caddr k))
(k (cadddr k)))
(let ((f v))
(Eval (app-rand M) env
(Push-app-a f k))))
We will find it convenient to also add a continuation with tag
stop which is used to halt computation. This is the
default initial continuation.
A register machine has a number of registers, an unbounded amount of memory, assignment to memory, assignment to registers, and a goto statement.
By now, the interpreter has only tail recursive calls to
Eval and to Pop; the different variations on
Push are all trivial. We now undertake the following
steps:
(define =M=) (define =env=) (define =k=)Thus, we can eliminate all parameters for procedures by assigning to the parameter registers instead. For instance, the interpreter looks like
(define Eval
(lambda ()
(cond
...)))
(define Main
(lambda (M)
(set! =M= M)
(set! =env= (empty-env))
(set! =k= 'stop)))
(define Eval
(lambda ()
((var? =M=)
(set! =k= =k=) ; redundant
(set! val (lookup =M= =env=))
(Pop))
((app? =M=)
(set! =k= (Push-app-f =M= =env= =k=))
(set! =M= (app-rator =M=)) ; destroys M!
(set! =env= =env=)
(Eval))
...))
(define val)
(define Pop
(lambda ()
(case (car k)
((push-app-f) (let ((M ...)
(env ...)
(k ...)
(f val))
(set! =k= (Push-app-a f k))
(set! =M= (app-rand M))
(set! =env= env)
(Eval)))
...)))
Now we're left only tail-recursive function calls. We can test this
in the following manner. First, we define Goto:
((call/cc
(lambda (k)
(set! Goto k)
(lambda () "hello world"))))
Now for each tail call, instead of writing (proc), we
write (Goto proc) instead.
Goto an appropriate name for the
above continuation? What does the (proc) to (Goto
proc) transformation help prove, and how?
Goto code does not work in DrScheme
because it evaluates each top level form in a separate control thread;
it does not permit control from one thread to cross to another one.
Hence, you cannot use the Goto form for tail calls in
course assignments.
cond,
set!, selector functions, Goto and the
Push-app-* procedures. All of these can be trivially
implemented at the machine level: most correspond directly to machine
instructions, while the Push procedures allocate enough
space to put the pointers in, and return a pointer to that newly
allocated memory. We have already seen how to implement such
procedures before with an array of memory. Hence, the only unnatural
assumption left in our interpreter is that we have an unlimited amount
of memory.
Consider the factorial function:
(define !
(lambda (n)
(if (= n 0)
1
(* n (! (- n 1))))))
First, we CPS it:
(define !
(lambda (n)
(!/k n (lambda (x) x))))
(define !/k
(lambda (n k)
(if (= n 0)
(k 1)
(!/k (- n 1) (lambda (v) (k (* n v)))))))
and then make it representationally independent:
(define !
(lambda (n)
(!/k n Stop)))
(define !/k
(lambda (n k)
(if (= n 0)
(Pop k 1)
(!/k (- n 1) (Push n k)))))
where
(define Stop
(lambda (x) x))
(define Push
(lambda (n k)
(lambda (m) (k (* n m)))))
(define Pop
(lambda (k m)
(k m)))
We can represent our continuations more directly by using a list.
This change only involves modifying the helper functions, leaving the
main code untouched:
(define Stop
'())
(define Push
cons)
(define Pop
(lambda (k m)
(if (null? k)
m
(Pop (cdr k) (* (car k) m)))))
It is easy to see that the lists we get will always consist of
numbers. In other words, Pop is Pi in
accumulator style. Hence, we can make the representation even more
concise:
(define Stop 1) (define Push *) (define Pop *)and voilà, we get the C version of
! in a
loop.
Shriram Krishnamurthi / shriram@cs.rice.edu