(define eval (lambda (e env) (variant-case e (Const (value) value) (Var (name) (or (lookup env name) (make-Prim name))) (Lam (formal body) (make-Closure formal body env)) (Ap (fun arg) (let ((vfun (eval fun env)) (varg (eval arg env))) (variant-case vfun (Prim (name) (cond ((eq? name 'not) (not varg)) ...)) (Closure (formal body env) (eval body (extend env formal varg)))))))))Environments are now manipulated in a stack-like manner: extended before evaluating the body of a procedure and retracted after. Furthermore, there's only ever one stack. So let's eliminate the
env
parameter to eval
, make it
a global definition, and put in explicit stack operations.
(define eval (lambda (e) (variant-case e (Const (value) value) (Var (name) (or (lookup env name) (make-Prim name))) (Lam (formal body) e) (Ap (fun arg) (let ((vfun (eval fun)) (varg (eval arg))) (variant-case vfun (Prim (name) (cond ((eq? name 'not) (not varg)) ...)) (Lam (formal body) (push! formal arg) (let ((v (eval body))) (pop!) v))))))))Notice that we have also gotten rid of
Closure
sine they
are now the same as Lam
. Using the first-order
implementation of environments, the stack operations are defined as
follows:
(define env (make-empty)) (define push! (lambda (x v) (set! env (make-extend-ff env x v)))) (define pop! (lambda () (variant-case env (extend-ff (fun dom ran) (set! env fun)))))This interpreter is more efficient: it does not have to pass
env
around as an argument, and closures are smaller.
(More importantly, we can explicitly free the extend-ff
records when we pop!
, where we can't
do this in the original interpreter because environments don't
behave like stacks. But we haven't yet discussed memory allocation
and reclamation.)
But what is the cost of this "simplification" we have performed? Consider:
(let* ((x 0) (f (lambda (y) (add1 x))) (x 2)) (f 0))In the original interpreter (or Scheme) this expression yields the value
1
. Let's run
it in our new interpreter. First we have to expand let*
.
A: ((lambda (x) B: ((lambda (f) C: ((lambda (x) D: (f 0)) 2)) (lambda (y) (add1 x))) 0))The environment stacks at each marked point in the above program are (top of stack on left):
A: [x = 0] B: [f = (lambda (y)...)] [x = 0] C: [x = 2] [f = (lambda (y)...)] [x = 0] D: [y = 0] [x = 2] [f = (lambda (y)...)] [x = 0]The evaluation of
(f 0)
occurs in environment D. Here
(lookup env x)
yields 2
. Thus the entire
expression evaluates to 3
. Clearly this is not equivalent
to the original interpreter. This new interpreter implements
dynamic scoping, while the original uses
lexical scoping. With dynamic scoping a variable's
binding may be superseded by another binding of the same name. With
lexical scoping, a variable's binding remains in effect forever.
Recall that earlier we said that names don't matter in Scheme. This is because Scheme is lexically scoped. In Scheme, or the original interpreter, the expression
(let* ((z 0) (f (lambda (y) (add1 z))) (x 2)) (f 0))has the same value as the expression above (all we've done is to rename the first bound variable
x
). But evaluating this
expression in the interpreter with dynamic scoping we get
1
, not 3
. With dynamic scoping, names DO
matter. Changing bound variables is not something we can
freely do.
If you think that this makes it harder to reason about programs in languages with dynamic scope, you're right. Nevertheless, for many years, Lisp systems were dynamically scoped. Emacs Lisp still is, as are many other programming languages whose designers did not understand this issue.
Implementing dynamic scoping using one global stack as above is called deep binding. An alternative way to implement dynamic scoping is to use a separate stack for each variable. This is called shallow binding. Lookup is cheaper because each current binding is at the top of the stack. But calling a procedure may be more expensive because it involves pushing and popping several stacks (and figuring out which stacks to push and pop).
output-port
, and the display routines write to that
port. You could then establish a new binding of output-port
just before calling P, which would be discarded when P returns.
Dynamic assignment is a way to accommodate bindings with dynamic extent into a lexically scoped language. It is quite simple: make a copy of the current value, change the variable to the new value, call P, restore the old value. Read: EOPL 5.7.2.
set!
with environments that map names to boxes. In addition,
we will put the result of eval
in a box.
The new boxes (and unboxing operations) are highlighted below:
(define eval (lambda (e env) (variant-case e (Const (value) (box value)) (Var (name) (box (unbox (lookup env name)))) (Lam (formal body) (box (make-Closure formal body env))) (Ap (fun arg) (let* ((vfun (unbox (eval fun env))) (varg (unbox (eval arg env)))) (variant-case vfun (Closure (formal body arg) (eval body (extend env formal (box varg))))))) (Set! (name body) (set-box! (lookup env name) (unbox (eval body env))) (box #f)) (Let (name binding body) (box (eval body (extend env name (box (unbox (eval body env))))))))))This new interpreter behaves the same as without the new boxes. Furthermore, we can eliminate the
(box (unbox ...))
around (lookup env name)
in Var
without changing anything. We'll call this interpreter B.
Exercise: Verify that interpreter B behaves the same as the original interpreter without boxed results.
Ap
the result of (eval arg env)
is unboxed, only
to be immediately boxed again when extending the environment
for the call. Suppose we "simplify" this:
(define eval (lambda (e env) (variant-case e (Const (value) (box value)) (Var (name) (lookup env name)) (Lam (formal body) (box (make-Closure formal body env))) (Ap (fun arg) (let* ((vfun (unbox (eval fun env))) (varg (eval arg env))) (variant-case vfun (Closure (formal body arg) (eval body (extend env formal varg)))))) (Set! (name body) (set-box! (lookup env name) (unbox (eval body env))) (box #f)) (Let (name binding body) (box (eval body (extend env name (box (unbox (eval body env))))))))))Now consider the following expression:
(let ((x 1) (f (lambda (a) (begin (set! a 2) a)))) (begin (f x) (display x)))In the original interpreter (or Scheme), this expression displays
1
. But in the interpreter above, it displays
2
. What's going on?
This new interpreter uses a different parameter passing
convention known as call-by-reference. In contrast, Scheme
uses call-by-value. Under call-by-reference, every value
"lives" in some location. A procedure call passes the
location of the argument value (or a reference to the value),
rather than the value itself. When the argument is a variable,
this variable's value can be modified by changing the value of
the corresponding formal parameter, because both the argument
value and the formal parameter are bound to the same location.
In the example above, the argument x
and
the formal parameter a
are bound to the same
location, so the assignment to a
changes
the value of x
.
Fortran is an example of a language that uses call-by-reference. Call-by-reference makes it much more difficult to reason about programs. Let us look at a few more examples:
(define f (lambda (a b) (set! a (+ a b)) (set! a (+ a b)) a)) (let ((x 1) (y 1)) (f x y)) (let ((z 1)) (f z z))With call-by-value,
f
returns
a + 2b
for any arguments. Hence the two
let-expressions yield 3 and 3 respectively.
But with call-by-reference, the first
let-expression yields 3, and the second yields 4.
The difference arises because in the second call the two
formal parameters of f
share the same location.
When two formal parameters share the same location they
are said to alias. Under call-by-value, aliasing
cannot occur because values are not locations.
(Caution: this doesn't mean aliasing cannot occur in
Scheme, see below.)
Another example:
(let ((f (lambda (a) (set! (add1 a))))) (f 1))A call to
f
increments the formal parameter
a
. But here the argument is a constant, which lives in
some location. So the value in the constant's location is changed!
Since our interpreter constructs a new box each time it evaluates a
constant, this doesn't matter. But most compilers "optimize"
evaluation of constants by constructing a single box for a particular
constant at compile time, and reusing the box every time the constant
is encountered. Then, with the code above in a loop, the next time
the constant 1
is encountered its box will have the value
2
. You can actually do this in many Fortran compilers.
A final example:
(define swap (lambda (a b) (let ((temp a)) (set! a b) (set! b temp)))) (let ((x 1) (y 2)) (swap x y))With call-by-value, calling
swap
has no effect.
But with call-by-reference, swap
does what its
name suggests: swaps the values of x
and y
.
(Ap (fun arg) (let* ((vfun (unbox (eval fun env))) (varg (eval arg env))) (variant-case vfun (Closure (formal body env) (let* ((b (box (unbox varg))) ; copy in (v (eval body (extend env formal b)))) (set-box! varg (unbox b)) ; copy out v)))))Exercise: Construct an expression that yields different results under call-by-value and call-by-value-result.
Typically languages use a mix of these conventions, based on the type of
each arg. Let's add boxes to the original call-by-value
interpreter represented as pairs with cdr #f
.
(variant-case vfun (Prim (name) (case name (make-box (cons varg) #f) (unbox (car varg)) (set-box! (set! ...)))) (Closure (formal body env) (eval body (extend env formal (box varg)))))Boxes are passed by reference, but other values by value. So where is the code to select the parameter passing based on type? It is done for us by Scheme, since since we are representing boxes as pairs, and Scheme passes pairs by reference.
Let's add pass-by-value pairs. Their operations are:
(variant-case vfun (Closure (formal body env) (eval body (extend env formal (box (if (vpair? varg) (vcons (vcar varg) (vcdr varg)) (varg)))))))Observe that when a vpair is passed to a function,
vset-car!
and vset-cdr!
operations on that pair will not affect the
argument pair. Doing this correctly for lists constructed
from vpairs requires a recursive procedure that copies
the entire list.