CS 22
Clab 33: The Meta-circular evaluator VI: user defined procedures
We will continue to follow SICP's design (using abstraction
barriers, etc.) to build a small Scheme evaluator bottom-up.
Our interpreter now lets users do most things except define and
execute their own functions. Today we will finish building the basic
mini-evaluator by adding user-defined procedures.
Let us review what Scheme should do when it evaluates an expression like:
(lambda (x y ) body)
According to rule 4 (second bullet on page 240), "A procedure is
created by evaluating a lambda expression relative to a given
environment. The resulting procedure object is a pair consisting of
the text of the lambda expression and a pointer to the environment in
which the procedure was created." This is done so that the procedure
can be properly applied when invoked on arguments. In our
interpreter, instead of 'a pointer to the environment in which the
procedure was created', we will carry that whole environment with the
procedure. That is evaluation of a lambda expression in environment
env will consist of constructing a list consisting of the word
procedure, followed by the lambda expression followed by the whole
environment, env. This can be accomplished by:
;;; returns #t if exp begins with lambda. returns #f otherwise.
(define (lambda? exp) (tagged-list? exp 'lambda))
;;; given a user-defined lambda expression, exp,
;;; returns the list of parameters of exp
(define (lambda-parameters exp) (cadr exp))
;;; given a user-defined lambda expression, exp,
;;; returns the body of exp
(define (lambda-body exp) (cddr exp))
;;; given a list of parameters and body and
;;; environment, env, make-procedure returns a procedure object that
;;; is a list consisting of the word procedure, followed by the
;;; list of parameters followed by the body followed by the whole environment
(define (make-procedure parameters body env)
(list 'procedure parameters body env))
Using we get:
> (define f '(lambda (x y z)
(+ (* x z) y)))
> f
(lambda (x y z) (+ (* x z) y))
> (lambda-parameters f)
(x y z)
> (lambda-body f)
((+ (* x z) y))
> (make-procedure
(lambda-parameters f)
(lambda-body f)
3levenv)
(procedure
(x y z)
((+ (* x z) y))
(((k y) isover 2)
((buchanan nader cat bush gore) (name too long) lost 56 maybeb maybeg)
((x cat p) 17 10 3)))
>
We can handle evaluation of lambda in mini-eval by adding the clause:
((lambda? exp)
(make-procedure (lambda-parameters exp)
(lambda-body exp)
env))
before the application clause. Do that, save, execute, and then
try the following:
Welcome to DrScheme, version 103p1.
Language: Graphical Full Scheme (MrEd) Custom.
> the-global-environment
(((car cdr cons null? + - * / = < > newline display)
(primitive #)
(primitive #)
(primitive #)
(primitive #)
(primitive #)
(primitive #)
(primitive #)
(primitive #)
(primitive #)
(primitive #)
(primitive #>)
(primitive #)
(primitive #)))
> (driver-loop)
MC-EVAL==> (define f
(lambda (x y) (+ (* x x) (* y y))))
ok
MC-EVAL==> f
#0=(procedure (x y) ((+ (* x x) (* y y))) (((f car cdr cons null? + -
* / = < > newline display) #0# (primitive #) (primitive
#) (primitive #) (primitive
#) (primitive #) (primitive
#) (primitive #) (primitive #)
(primitive #) (primitive #) (primitive
#>) (primitive #) (primitive
#))))
MC-EVAL==> (quit)
"exiting-mini-eval"
> the-global-environment
#1=(((f car cdr cons null? + - * / = < > newline display)
(procedure (x y) ((+ (* x x) (* y y))) #1#)
(primitive #)
(primitive #)
(primitive #)
(primitive #)
(primitive #)
(primitive #)
(primitive #)
(primitive #)
(primitive #)
(primitive #)
(primitive #>)
(primitive #)
(primitive #)))
>
Notice that f is bound to the list:
(procedure (x y) ((+ (* x x) (* y y))) #1#)
where #1# is an abbreviation for the environment that the lambda was
evaluated in (later mutated by the define f ).
Of course, now that we have procedure objects that look like:
(procedure
(x y z)
((+ (* x z) y))
(((k y) isover 2)
((buchanan nader cat bush gore) (name too long) lost 56 maybeb maybeg)
((x cat p) 17 10 3)))
we need a predicate to recognize 'em and some selectors
to pull them apart so define:
;;; returns #t if p is a user-defined procedure object
;;; returns #f otherwise
(define (compound-procedure? p)
(tagged-list? p 'procedure))
;;; returns list of formal parameters from procedure object p
(define (procedure-parameters p) (cadr p))
;;; returns the body of procedure object p
(define (procedure-body p) (caddr p))
;;; returns environment bound with procedure object p
(define (procedure-environment p) (cadddr p))
If this is the way we create procedure objects, we will have to make
sure that we apply them according to rule 3 (first bullet on page
240): "A procedure object is applied to a set of arguments by
constructing a frame, binding the formal parameters of the procedure
to the actual values of the arguments of the call, and then evaluating
the body of the procedure in the context of the new environment
constructed. The new frame has as its enclosing environment the
environment part of the procedure object being applied."
We carry this rule out by redefining mini-apply. We use the predicate
and selectors defined above. In defining mini-apply, we take care of
"binding the formal parameters of the procedure to the actual values
of the arguments of the call ... The new frame has as its enclosing
environment the environment part of the procedure object being
applied." by
(extend-environment
(procedure-parameters procedure)
arguments
(procedure-environment procedure))))
where arguments was obtained by our old friend: list-of-values.
The body of the procedure object is: (procedure-body procedure).
We take care of "and then evaluating the body of the procedure in the
context of the new environment constructed" by
(eval-sequence
(procedure-body procedure)
(extend-environment
(procedure-parameters procedure)
arguments
(procedure-environment procedure))))
Except for the addition of the lambda clause, mini-eval can be left as
it was for handling primitive procedures. But we need to modify
mini-apply so that it looks like:
(define (mini-apply procedure arguments)
(cond ((primitive-procedure? procedure)
(apply-primitive-procedure procedure arguments))
((compound-procedure? procedure)
(eval-sequence
(procedure-body procedure)
(extend-environment
(procedure-parameters procedure)
arguments
(procedure-environment procedure))))
(else
(error
"Unknown procedure type -- APPLY" procedure))))
Try this out. Here are a couple of examples:
(driver-loop)
MC-EVAL==> (define f
(lambda (x) (* x x)))
ok
MC-EVAL==> (f 5)
25
MC-EVAL==> ((lambda (x y) (+ x (f y))) 3 20)
403
MC-EVAL==> q
> (driver-loop)
MC-EVAL==> (define fact
(lambda (n)
(cond
((< n 1) 1)
(else (* n (fact (- n 1)))))))
ok
MC-EVAL==> fact
#0=(procedure (n) ((cond ((< n 1) 1) (else (* n (fact (- n 1)))))) (((fact car cdr cons null? + - * / = < >) #0# (primitive #) (primitive #) (primitive #) (primitive #) (primitive #) (primitive #) (primitive #) (primitive #) (primitive #) (primitive #) (primitive #>))))
MC-EVAL==> (fact 3)
6
MC-EVAL==> (fact 5)
120
Now I must shout:
You now have a small but complete evaluator. With appropriate
encoding and memory resources, this evaluator can do anything
that any computer can do using any language.
Kelemen seconds three MIT professors (Abelson, Sussman, Sussman)
when they say:
"An evaluator (or interpreter) for a programming language is
a procedure that, when applied to an expression of the language,
performs the actions required to evaluate that expression.
It is no exaggeration to regard this as the most fundamental
idea in programming:
The evaluator, which determines the meaning of expressions in a
programming language, is just another program.
To appreciate this point is to change our images of ourselves
as programmers. We come to see ourselves as designers of languages,
rather than only users of languages designed by others."
If you have not already done so, modify your interpreter so that
it can handle the sequencing construct:
begin and the (define (f ... form of define.
Try your new interpreter on the procedure make-withdraw of pp. 222.
I used cond because I didn't have if implemented. Make sure you have
* as a primitive procedure.
See if you can get:
> (driver-loop)
MC-EVAL==> (define (fact n)
(cond
((< n 1) 1)
(#t (* (fact (- n 1)) n))))
ok
MC-EVAL==> (fact 6)
720
MC-EVAL==> (define (make-withdraw balance)
(lambda (amount)
(cond
((> balance amount)
(set! balance (- balance amount))
balance)
(else "insuff funds"))))
ok
MC-EVAL==> (define w1 (make-withdraw 100))
ok
MC-EVAL==> (w1 20)
80
MC-EVAL==> (define w2 (make-withdraw 200))
ok
MC-EVAL==> (w2 20)
180
MC-EVAL==> q
Unbound variable q
>