eval
follows. The purpose of this procedure is to evaluate a Micro-Scheme
expression.
(define eval (lambda (e env) (variant-case e (Const (value) value) (Var (name) (or (lookup env name) (lambda (x) (cond ((eq? name 'not) (not x)) ...)))) (Lam (formal body) (lambda (arg) (eval body (extend env formal arg)))) (Ap (fun arg) (let ((vfun (eval fun env)) (varg (eval arg env))) (vfun varg))))))Here
e
is the expression to be evaluated in environment
env
. We need the environment parameter to record
bindings for procedure arguments.
(In Micro-Scheme functions can only take one parameter.)
This is a meta-circular interpreter. This term refers to using the various constructs of Scheme to implement substantially the same constructs in Micro-Scheme. For instance, we use lambda to represent Micro-Scheme procedures as procedures, and application to apply Micro-Scheme procedures. Provided you understand Scheme, this interpreter provides a precise explanation for how (Micro-) Scheme works.
(define-record Prim (name)) (define-record Closure (formal body env)) (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)))))))))
letrec
, to be sure we understand it.
The additional clause we need is:
(Letrec (name exp body) (letrec ((env2 (extend env name (eval exp env2)))) (eval body env2)))We must run the
body
in an environment
env2
that
includes a binding of name
to the value
of exp
. To get the value of exp
,
we must evaluate it in this same extended environment
env2
.
This would be a great explanation if it worked, but it doesn't.
The problem is that we have to run the application
(eval exp env2)
before we know the value of
env2
.
Recall that we are representing Micro-Scheme procedures as Scheme procedures. For now, let's restrict Micro-Scheme to require that the expression bound by a letrec be a lambda. Ie., the syntax for Micro-Scheme's letrec expression is:
(letrec ((x (lambda ...)) body))Then
(eval exp env2)
must return a procedure
(of one argument), since exp
is a lambda-expression.
Therefore, we can wrap (eval exp env2)
with a
lambda without changing the value it will compute:
(Letrec (name exp body) (letrec ((env2 (extend env name (lambda (x) ((eval exp env2) x))))) (eval body env2)))Now everything works, because we can compute
(extend env name (lambda (x) ((eval exp env2) x)))
without knowing the value of env2
.
(define-record Rec (exp env)) ... (Letrec (name exp body) (letrec ((env2 (extend env name (make-Rec exp env2)))) (eval body env2))) (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))) (Rec (exp env) (eval exp env))))))))Right?? Wrong! Once again, we need to know the value of
env2
to evaluate (make-Rec exp env2)
.
You have to be careful with the first-order transform
when recursion is involved.
env
and the evaluation of exp
into a single operation rec-extend
.
(define-record rec-extend-ff (f dom exp)) (define rec-extend make-rec-extend-ff) ... (Letrec (name exp body) (let ((env2 (rec-extend env name exp))) (eval body env2)))We must also change
lookup
.
(define lookup (lambda (env x) (variant-case env (empty-ff () #f) (extend-ff (fun dom ran) (if (eq? x dom) ran (lookup fun x))) (extend-record-ff (fun dom exp) (if (eq? x dom) (eval exp env) (lookup fun x))))))All is now correct.
exp
until it is used and repeats the evaluation each time.
What if we were to do the following?
env
must map names to boxes. Let's first
transform our first-order interpreter without letrec to
one using boxes.
(define eval (lambda (e env) (variant-case e (Const (value) value) (Var (name) (let ((b (lookup env name))) (if b (unbox b) (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? 'not name)) (not varg) ...)) (Closure (formal body env) (eval body (extend env formal (make-box varg))))))))))This new interpreter behaves the same as the old one. Now let's add
letrec
, using our placeholder idea:
(Letrec (name exp body) (let* ((b (make-box #f)) (env2 (extend env name b)) (v (eval exp env2))) (set-box! b v) (eval body env2)))Hey, it works!
By the way, this new interpreter does not have the limitation that the
right-hand side of a letrec
must be a lambda expression.
We can use any expression as the right-hand side of a letrec so
long as it doesn't use the variable before we've computed
its value. Unfortunately, this condition is impossible to
check. Hence the set of legal Scheme programs is undecidable.
(set! x e)Here
x
must be a variable. Let's try to support
set!
in our interpreter.
(Set! (name body) (begin (set-box! (lookup env name) (eval body env)) #f))Here we've chosen to return
#f
as the value of
a set!
expression. In Scheme the result of
set!
is unspecified. Its a bad idea
to use the result of set!
.
We could add the box-related functions to our interpreter,
but let's instead build them from set!
.
(define-record Boxrep (unbox set-box!)) (define make-Box (lambda (v) (make-Boxrep (lambda () v) (lambda (new set! v new))))) (define unbox (lambda (b) ((Boxrep->unbox b))))