Lecture
18: Explaining |
|
|
|
letcc
and
error
Our goal is to produce a tail-recursive interpreter, which we will use to understand
the meaning of error
and letcc
.
To recap our previous lecture, during its evaluation, every phrase will be surrounded by some computation that is waiting to be performed (and, typically, that depends on the value of this phrase). This remaining computation is known as an evaluation context. Turning this evaluation context into a function is the act of making the continuation explicit.
For instance, in
(+ (* 12 3) (- 2 23))
the evaluation context of the first sub-expression (assuming it is evaluated first) is
(+ _ (- 2 23))
(where we pronounce _
as ``hole''), so the reified version of this
context is
(lambda (x) (+ x (- 2 23)))
Puzzle: Can (lambda (x) ... _ ...) be
a valid evaluation context? |
error
Let us consider an interpreter for LC:
(define Eval (lambda (M env) (cond ((var? M) (lookup M env)) ((lam? M) (make-closure M env)) ((app? M) (Apply (Eval (app-rator M) env) (Eval (app-rand M) env))) ((add? M) ...) ...)))
In this interpreter, we see both the creation of new continuations and the use
of the (implicit) continuation. New continuations are created in the code for
applications, at the evaluation of the sub-expressions and again in the call to
Apply
. The other two clauses shown use the current continuation by
passing it a value.
We now use the standard technique for transforming Scheme code to transform the interpreter into CPS:
(define Eval/k (lambda (M env k) (cond ((var? M) (k (lookup M env))) ((lam? M) (k (make-closure M env))) ((app? M) (Eval/k (app-rator M) env (lambda (rator-v) (Eval/k (app-rand M) env (lambda (rand-v) (Apply/k rator-v rand-v k)))))) ...)))
We need to similarly transform Apply
. Whereas before we had
(define Apply (lambda (f a) (cond ((closure? f) (Eval (body-of f) (extend (env-of f) (param-of f) a))) (else ...))))
we now have
(define Apply/k (lambda (f a k) (cond ((closure? f) (Eval/k (body-of f) (extend (env-of f) (param-of f) a) k)) (else ...))))
Historical Note: |
None of the Apply procedures take
an environment as an argument; instead, they choose to use the one
stored in the closure. However, it is possible to imagine a different
semantics that passes on the current environment to
Apply , which then passes it on for the remainder of the
evaluation. Such a system is said to have dynamic binding,
as opposed to the static binding used here. |
Exercise: |
Implement dynamic binding. |
We have intentionally left the fall-through case of the cond
expressions
in the Apply
procedures empty. However, we know from before that
there should be a call to error
in that slot. Since we want to
explain error
, we need to determine how to signal an erroneous
application.
Since the k
argument always represents the entire evaluation
context, there is no additional computation awaiting the value returned by the
interpreter. Therefore, to ignore the pending computations and return a value
directly, all that error
needs to do in this interpreter is to
return a value. This value is then returned to the user. (Note that this crucially
depends on the fact that the interpreter is fully tail-recursive.) Therefore,
the code for Apply/k
could be written as
(define Apply/k (lambda (f a k) (cond ((closure? f) (Eval/k (body-of f) (extend (env-of f) (param-of f) a) k)) (else 'ouch!))))
Note that the else
clause above is ``returning'' at the meta-level,
not at the level of LC. A function return in LC is modeled by passing the
result of evaluating the function call to the appropriate continuation.
We have teased out control entirely and made it a distinct entity in the interpreter. Thus, we can use it as before, we can ignore it, or we can harness it in new ways, as we will see below.
Puzzle: What value would we use as the initial continuation? |
letcc
In our earlier interpreters, we modeled letcc
by appealing to
the letcc
form at the meta-level:
(cond ... ((letcc? M) (letcc k (Eval (body-of M) (extend env (label-of M) k)))) ...)
Since we want letcc
to bind a program variable to the rest of the
computation, we could instead write, in Eval/k
,
(cond ... ((letcc? M) (Eval/k (body-of M) (extend env (label-of M) k) k)) ...)
This rewrite makes manifest an interesting feature of control. Just as stores
were duplicated when we made them explicit, here we have two uses of
k
. In the case of stores, we hypothesized that we could have an operator
that ``forgot'' the side-effects performed in the evaluation of an expression,
but we found no use for such an operator. Now, however, we can save the evaluation
context, and return to it -- ignoring any intermediate context -- if we wish.
Furthermore, since this evaluation context is bound in the environment, we can
return to it whenever we choose to, even outside the lexical context
of the letcc
expression. One example of where this might be useful
is in the implementation of context switches during multitasking, where
we periodically store the current context and return to it later.
Hence, whereas before control was single-threaded, and could be implemented as a stack, it is now tree-shaped and cannot be implemented in that manner.
Earlier, we decided to eliminate meta-language closures from our interpreter.
Now, we have reintroduced them in the process of CPSing our interpreter, ie,
in the action for applications. As before, we shall model this process of closure
creation abstractly by using the procedure Push
. Similarly, we
will abstract over the use of the continuation -- to ``return'' values -- as
the procedure Pop
.
(define Eval*/k (lambda (M env k) (cond ((var? M) (Pop k (lookup M env))) ((lam? M) (Pop ...)) ((app? M) (Eval (app-rator M) env (Push 1 M env k))) ...)))
To begin with, these abstractions can map to the current implementation method:
(define Push (lambda (name M env k) (lambda (x) (Eval/k (app-rand M) env (lambda (y) (Apply/k x y k)))))) (define Pop (lambda (k v) (k v)))
Note, further, that Push
has a call to Eval/k
; we rewrite
this as
(Eval/k (app-rand M) env (Push 2 x k))
In all, there is a finite number of locations that create continuations in the
interpreter, so we can write a procedure that creates each one of these. Thus,
Push
can be rewritten as
(define Push (lambda (name . pv) (cond ((= name 1) (let ((M ...pv...) (env ...pv...) (k ...pv...)) (lambda (x) (Eval/k (app-rand M) env (Push 2 x k))))) ((= name 2) (let ((x ...pv...) (k ...pv...)) (lambda (y) (Apply/k x y k)))))))
Finally, the remaining reliance on lambda
can be removed by replacing
it with a call to list
instead. In the process, Pop
has to become more elaborate: in particular, it needs to dispatch on the ``name''
of the current continuation. This is analogous to the change made in Apply
when we moved from a meta-level to a structure-based representation of closures
in our original interpreters.