CS 37
Clab 31: 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.
Before we proceed, I would like you to read the following obituary
from yesterday's NY Times.:
Amir Pnueli, Pioneer of Temporal Logic, Dies at 68 or
pnueli local
Time is one of the things that makes some aspects of computer science
more difficult than ordinary mathematics. Without explicitly
mentioning time, it was an issue when we considered the difference
between recursive processes and iterative processes.
Even though they
may both be described by a recursive procedure, the processes those
procedures engender may play out differently in time. We treated time
more explicitly once we introduced mutation (set!, order is important),
threads, and
streams (future promises). In addition, to the mentions of time
in the main text, the following footnotes involve time one way
or another:
ch1: 54
ch3:12,30,34,35,36,39,47,48,49,50,51,52,58,59,73,75.
Treating time formally is not part of an ordinary undergraduate
curriculum in CS nor even most areas of graduate study. However,
progress in this area has already made significant contirbutions
to processor design and will hopefully continue to improve
correctness of software.
Last clab, we added begin and cond to our interpreter. If you did
not finish, work on it some more at the end of this
clab and at home. Until
we get to the final examples, most of what follows will not need
cond.
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, it will look like we are carrying the whole environment
along with a procedure. That, however, is due to our printing. The
identifier env at the end of our procedure objects is in fact
'a pointer to the environment in which the
procedure was created'.
So, 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, 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))
Put those in your definitions window, hit run and test in
your interactions window.
You should 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)
((Williams Amherst Swarthmore Middlebury cat) 3 2 1 4 4)
((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, run, and then
try the following:
> 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)
((Williams Amherst Swarthmore Middlebury cat) 3 2 1 4 4)
((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,
in the call to mini-apply from mini-eval.
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 which we
already did, 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))))))
#1=(((fact f car cdr cons null? + - * / = < > newline display) #0#
(procedure (x) ((* x x)) #1#) (primitive #) (primitive
#) (primitive #) (primitive
#) (primitive #) (primitive
#) (primitive #) (primitive #)
(primitive #) (primitive #) (primitive
#>) (primitive #) (primitive
#))))
MC-EVAL==> (fact 3)
6
MC-EVAL==> (fact 5)
120
Note that f was a free variable in the (lambda (x y) .. ) definition.
f was found properly even though f was NOT in the frame binding
x and y to apply the lambda to 3 20.
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==> (w1 20)
60
MC-EVAL==> (define w2 (make-withdraw 200))
ok
MC-EVAL==> (w2 20)
180
MC-EVAL==> (w1 50)
10
MC-EVAL==> (w2 50)
130
MC-EVAL==> (w1 50)
insuff funds
MC-EVAL==> (w2 50)
80
MC-EVAL==> (quit)
"exiting-mini-eval"
>
If you do not successfully finish this in clab today, work on
it at home. Feel free to ask questions.