CS 22 Mini-Evaluator Project
Part 5: lambda and let expressions

Introduction

When Scheme evaluates an expression like (lambda (x y) (+ x y a)), it creates a procedure, which can be applied later to some arguments. This procedure contains two pieces of information: (a) the original lambda expression from which it was created, and (b) the environment in which it was created. Should the procedure ever get applied to some arguments (10 and 20, let's say), two things will happen:
  1. The environment saved in the procedure will get extended temporarily to include bindings of each of the procedure's parameters (which are x and y in the above example) to the values of the arguments (10 and 20).
  2. The body of the procedure's lambda expression (which is (+ x y a) in the above example) will get evaluated with respect to this extended environment, and the resulting answer will serve as the final answer for the procedure application.
For example, suppose we do the following:
MINI-EVAL> (define a 100)
a
MINI-EVAL> (define proc (lambda (x y) (+ x y a)))
proc
This will create a procedure (called proc) that contains the lambda expression itself and an environment. This environment contains a binding for a (to the value 100). If we then apply proc to 10 and 20 by evaluating the application expression (proc 10 20), the bindings (x . 10) and (y . 20) will be added temporarily to the environment and the expression (+ x y a) will be evaluated with respect to this new environment (which now includes bindings for all of the symbols x, y, and a). Evaluating (+ x y a) will in turn cause the evaluator to recursively call itself, until eventually the primitive + procedure is applied and the value 130 is returned as the final result.

Save a copy of the Part 5 code before you begin.

Handling lambda expressions

In our interpreter, the evaluation of a lambda expression in an environment will consist of constructing a tagged list containing the symbol lambda-procedure (so that we can distinguish it from a primitive procedure), followed by the lambda expression itself, followed by the environment used to evalute the lambda expression. The following procedure will do this for us:

;;; Constructor for procedures created with lambda:
(define make-lambda-procedure
   (lambda (lambda-exp env)
     (list 'lambda-procedure lambda-exp env)))

Here is an example of what happens in Scheme when we evaluate a lambda expression:

> (lambda (x y) (+ x y a))
#[procedure]
Although we can't see it, this procedural object actually contains within it a representation for the environment in which the lambda expression was evaluated. For comparison, here is what should happen when we use our own mini-evaluator:
MINI-EVAL> (lambda (x y) (+ x y a))
(lambda-procedure 
   (lambda (x y) (+ x y a)) 
   (((car primitive car #[procedure car]) ...)))
Every time our interpreter evaluates a lambda expression, it should create a lambda procedure represented as a list of three items like that shown above. We can accomplish this by adding the following line to mini-eval:
((lambda? exp) (make-lambda-procedure exp env))
Define the tester lambda?, which checks if an expression is a lambda expression. Next, we need some helper funtions for dealing with lambda procedures. It is very important to keep in mind the distinction between a lambda expression such as (lambda (x y) (+ x y a)) and a lambda procedure, which is an object created by the evaluator as the result of evaluating a lambda expression. Define the following four testers and selectors for lambda procedures:
;;; Distinguishes between primitive procedures and lambda 
;;; procedures by looking at the tag at the front of the object.
(define lambda-procedure? 
  (lambda (proc)
     ...))

> (lambda-procedure? '(primitive car #[procedure car]))
#f
> (lambda-procedure? '(lambda-procedure (lambda (x) (+ x 2)) global-environment)
#t

;;; Returns a lambda procedure's list of parameter names.
(define procedure-parameters 
  (lambda (proc)
    ...))

> (procedure-parameters '(lambda-procedure (lambda (x y) (+ x y)) global-environment)
(x y)
> (procedure-parameters '(lambda-procedure (lambda () 'hello) global-environment)
()

;;; Returns a list of all the expressions in the body of the 
;;; procedure's lambda expression:
(define procedure-body 
  (lambda (proc)
    ...))

> (procedure-body '(lambda-procedure (lambda (x y) (+ x y)) global-environment))
((+ x y))
> (procedure-body '(lambda-procedure (lambda () (display "hi") (newline)) global-environment))
((display "hi") (newline))

;;; Returns the environment saved in a lambda procedure:
(define procedure-env 
  (lambda (proc)
    ...))

> (procedure-env '(lambda-procedure (lambda (x y) (+ x y)) ()))
()

Handling lambda applications

Now we have to make sure that we apply these lambda procedures correctly. Go back and re-read the description of procedure application in the Introduction.

Let's go through this process again step by step, using a different example. Consider the process of evaluating the following expression which should return 0.

((lambda (x) (+ 10 x)) (* -1 10))
First, mini-eval will recognize this expression as a procedure application. It will then (recursively) evaluate the operator (lambda (x) (+ 10 x)) using the current environment, evaluate the list of operands ((* -1 10)) using the current environment, and then call mini-apply to apply the resulting lambda procedure to the list (-10). The procedure obtained by evaluating the operator will look like this:
(lambda-procedure (lambda (x) (+ 10 x)) (((car primitive ...))))
In mini-apply, we will need to include a new cond clause to handle the case of applying a lambda. The new line should look like this:
((lambda-procedure? proc) (apply-lambda-procedure proc vals))
The new procedure apply-lambda-procedure should implement the steps described above. In particular, apply-lambda-procedure should first extend the environment stored in the procedure using the procedure's parameters and the list of values. Then it should evaluate the body of the procedure in the context of that new environment. In this case, a new frame containing the single binding (x . -10) will be added to the procedure's environment, and then the sequence of expressions in the body ((+ 10 x)) will be evaluated in that new context, yielding a final result of 0.

Remember that the body of a procedure is a list of expressions to be evaluated, so you cannot use mini-eval directly on the body. Instead, you must call mini-eval on each expression, in order from first to last. In the above example, the body contains just one expression, but it is still a list. The result of evaluating the last expression in the list should serve as the result of evaluating the body. You should be able to handle the evaluation of the procedure body in the same way that you handled evaluating the expressions in a begin in Part 4.

Make sure you clearly understand these steps before you try to update mini-apply and write apply-lambda-procedure. Once you have implemented lambda procedure application in your interpreter, try the following tests to see if things are working properly:

MINI-EVAL> (define animal 'dog)
animal
MINI-EVAL> ((lambda (new)
               (display "original animal was ")
               (display animal)
               (newline)
               (set! animal new) 
               (display "updated animal to ")
               (display animal)
               (newline)
               'done)
              'cat)
original animal was dog
updated animal to cat
done
MINI-EVAL> animal
cat
MINI-EVAL> (define count-down
              (lambda (n)
                (if (= n 0)
                  'boom!
                  (begin
                    (display n)
                    (newline)
                    (count-down (- n 1))))))
count-down
MINI-EVAL> (count-down 5)
5
4
3
2
1
boom!
Trace mini-eval and mini-apply as you try the above examples. Also try a number of your own examples, such as factorial, square, and so on.

Before we go on to let expressions, first consider what happens when we do the following:

MINI-EVAL> (define myadd1 (lambda (x) (+ x 1)))
myadd1
MINI-EVAL> myadd1
?
The define expression binds the symbol myadd1 to a lambda procedure in the global environment. So evaluating myadd1 causes our evaluator to look up the value of this variable in the global environment. That value is a list of the form:
(lambda-procedure lambda-expression environment)
More precisely, it will look like:
(lambda-procedure
   (lambda (x) (+ x 1))
   (((myadd1 lambda-procedure (lambda (x) (+ x 1)) (((myadd1 ...)
Try this in your evaluator and watch what happens (if necessary, use the stop button to interrupt the result). What's the problem? We've created a circular structure! The symbol myadd1 is bound to the above object in the global environment, but that object itself includes the global environment as part of its structure, so in some versions of Scheme we get into an infinite loop just by trying to print it out.

Since our users should be able to evaluate a lambda expression and see the result without worrying about infinite loops, we need to work around this problem. Change driver-loop so that instead of calling pretty-print on the result of mini-eval, it calls the new procedure safe-print, which you will write. The procedure safe-print should check if the object to be printed is a lambda procedure, and if so, it should print only the procedure's parameters and body, but not its environment. If the object to be printed is a primitive procedure, it should avoid printing the primitive's internal implementation procedure. For all other types of objects, safe-print should just call pretty-print on the object. Write this procedure now. Below are some examples of how it should work:

MINI-EVAL> (define myadd1 (lambda (x) (+ x 1)))
myadd1
MINI-EVAL> myadd1
[lambda-procedure (x) ((+ x 1))]
MINI-EVAL> (define smaller (lambda (x y) (if (< x y) x y)))
smaller
MINI-EVAL> smaller
[lambda-procedure (x y) ((if (< x y) x y))]
MINI-EVAL> car
[primitive-procedure car]
MINI-EVAL> 'hello
hello
For a more rigorous test of our evaluator's ability to handle procedures, we can try out some of the object-oriented code from Chapter 3 of the textbook:

MINI-EVAL> (define make-withdraw
              (lambda (balance)
                (lambda (amount)
                 (if (> balance amount)
                   (begin
                     (set! balance (- balance amount))
                      balance)
                       "Insufficient funds"))))
make-withdraw
MINI-EVAL> (define w1 (make-withdraw 50))
w1
MINI-EVAL> (w1 70)
"Insufficient funds"
MINI-EVAL> (w1 30)
20
MINI-EVAL> (define w2 (make-withdraw 100))
w2
MINI-EVAL> (w2 70)
30

Handling let expressions

A let expression has the following syntax:

(let ((var1 val1) (var2 val2) ... (varn valn))
  exp1
  exp2
   ...
  expm)
This is simply shorthand for the following:
((lambda (var1 var2 ... varn) exp1 exp2 ... expm)
  val1 val2 ... valn)
Modify your interpreter so that it can recognize and correctly handle let expressions. You should write procedures called let? and let->lambda. The procedure let->lambda should take a let expression and return an equivalent application expression as specified by the above syntactic equivalence rule. When mini-eval encounters a let expression, all it needs to do is transform the expression into an equivalent application expression (using let->lambda), and then just call itself again on the new expression. Try the following tests when you are done:
MINI-EVAL> (let ((a 10))
              (let ((b (lambda (x) (+ x a))))
                (let ((a 500))
                  (b a))))
510
MINI-EVAL> (let ((f car))
              (let ((f (lambda (y) (f y))))
                (f '(apple banana cherry))))
apple
MINI-EVAL> (define x 1)
x
MINI-EVAL> (let ((x 0) (y 2) (z x))
              (set! x 10)
              (+ x y z))
13
MINI-EVAL> x
1
MINI-EVAL> (let ((z (* x -1))) 
              (if (> z 0) 'positive 'negative))
negative
MINI-EVAL> z
*** ERROR -- unbound variable z