Tags SICP

A reminder for folks who’ve reached this page directly: this is part of my SICP reading and exercise solving project, introduced here. The code for chapter 4 is written in Common Lisp.

Exercise 4.25

In ordinary applicative-order Scheme, with the definition of unless as provided, the call (factorial 5) will enter an infinite loop. This will happen because this unless in applicative-order Scheme evaluates its exceptional-value argument in any case, disregarding the value of condition. Therefore, no matter what the value of condition is, the call (factorial 5) executes (factorial 4), which in turn executes factorial for 3, 2, 0, -1, -2 ... and so on ad-(minus)-infinitum.

In normal-order evaluation this will work, because (factorial 1) won’t call factorial for 0, as unless won’t evaluate exceptional-value when condition is true.

Exercise 4.26

Ben’s side:

unless can be very easily implemented in our Scheme evaluator, as a derived expression that’s transformed into if. This information is much simpler than cond->if which exists in the evaluator’s code. We’ll add this clause to eval.:

  ((unless? exp)
    (eval. (unless->if exp) env))

And the implementation is:

(defun unless? (exp) (tagged-list? exp 'unless))
(defun unless-predicate (exp) (cadr exp))
(defun unless-consequent (exp)
  (if (not (null (cdddr exp)))
    (cadddr exp)
    'false))
(defun unless-alternative (exp) (caddr exp))

(defun unless->if (exp)
  (make-if 
    (unless-predicate exp)
    (unless-consequent exp)
    (unless-alternative exp)))

Alyssa’s side:

Boy, I can’t imagine any really useful application of unless as a higher-order procedure. I have no idea what the book authors had in mind here. In Common Lisp, for instance, unless is a macro and therefore can not be used as a procedure1

Exercise 4.27

(define w (id (id 10)))
;;; L-Eval input:
count
;;; L-Eval value:
1

Because so far the call to id was forced only once, in the outer call. The argument to it, (id 10) was not evaluated because it’s not needed yet.

;;; L-Eval input:
w
;;; L-Eval value:
10

The evaluation of w forces both calls to id. Since the inner call returns 10, the outer does too.

;;; L-Eval input:
count
;;; L-Eval value:
2

Since, as I mentioned, the evaluation of w forced the second id call as well, count was incremented twice.

Exercise 4.28

Consider this code:

(interpret
  '(define adder (lambda (x) (+ x 1))))

(interpret
  '(define (doer func arg)
    (func arg)))

(interpret
  '(doer adder 5))

With the way the lazy evaluator is currently implemented, this works and prints 6 as expected. However, had we not forced the operator before passing it to apply., this would throw an error.

Why? Because in execution of (func arg) in doer, the operator func is passed in as an argument. When we call (doer adder 5), the arguments of doer are not forced, and so func is not an actual value but a thunk.

Exercise 4.29

Even the simple factorial computation function would exhibit worse performance without memoization of arguments:

(interpret
  '(define (fact n)
    (if (= n 0) 
      1
      (* n (fact (- n 1))))))

(time (interpret '(fact 150))) ; with memoization
=> 0.109 sec

(time (interpret '(fact 150))) ; without memoization
=> 4.5 sec

Each time fact is called recursively, memoization saves the operation of obtaining the code from the call.

Also, with memoization:

(square (id 0))
=> 100
count
=> 1

Without memoization:

(square (id 0))
=> 100
count
=> 2

It’s clear why this happens. In square, x is the same object in both arguments to the multiplication operator. With memoization, its value is computed only once and is obtained from the cache the second time – hence count gets incremented only once.

Exercise 4.30

a. eval. is called on each expression of the sequence. Since for-each uses begin which is evaluated as a sequence, each iteration of the for-each is evaluated with eval. – and since display is a primitive procedure, the actual value of its argument is eventually computed.

b. With the original eval-sequence:

(p1 1)
=> (1 2)
(p2 1)
=> 1

Let’s see what happens in p2. The crucial point here is the call to the internal function p. When the call is executed, as with any application of a compound procedure, the body of p is evaluated with the environment extended with its arguments tied to their delayed values. The body of p is evaluated in sequence. When e is evaluated, the assignment thunk is substituted instead of it, but since its value isn’t passed to any primitive procedure, it is not forced and the assignment doesn’t really happen. So later, when x is evaluated and returned, it has its old value. To understand it better, we’ll rewrite p2 as follows2:

(define (p2 x)
  (define (p e)
    e
    (print e)
    x)
  (p (set! x (cons x '(2)))))

Now:

(p2 1)
=> OK (1 2)

What has happened here ? The call (print e) passes e to a primitive procedure, and that forces it. While it’s being forced, it assigns a new value to x.

Using Cy’s proposed eval-sequence:

(p1 1)
=> (1 2)
(p2 1)
=> (1 2)

Because e is forced in the sequence even without being passed to a primitive procedure.

c. As I explained in the answer to a., the calls to display force the arguments anyway because display is a primitive procedure. So calling actual-value on them can’t hurt.

d. I like Cy’s approach because it behaves better when there are statements with side effects.

Exercise 4.31

While at first this exercise looks intimidating, it really isn’t that difficult once you sit down and start coding. I highly recommend trying to solve it on your own – I found the process very interesting and educational.

First of all, let’s add new “syntax recognizers” for the lazy operands of procedure applications. Now each operand can be either an atom (a Lisp name for a “scalar”, or non-list: symbols, numbers, etc.) or a list, which means it’s a lazy operand:

(defun evaluated-operand? (op) (atom op))

(defun lazy-operand? (op) 
  (and 
    (consp op) 
    (or
      (eql (cadr op) 'lazy)
      (eql (cadr op) 'lazy-memo))))

(defun lazy-memo-operand? (op)
  (and (consp op) (eql (cadr op) 'lazy-memo)))

(defun lazy-operand-name (op) (car op))

Also, to support both memoized and unmemoized thunks we’ll add the thunk-memo type:

(defun delay-it-memo (exp env) (list 'thunk-memo exp env))
(defun thunk-memo? (obj) (tagged-list? obj 'thunk-memo))

And force-it will have to be changed to accomodate this difference:

(defun force-it (obj)
  (cond ((thunk-memo? obj)
          (let ((result 
                  (actual-value
                    (thunk-exp obj)
                    (thunk-env obj))))
            (setf (car obj) 'evaluated-thunk)
            (setf (car (cdr obj)) result)
            (setf (cdr (cdr obj)) '())
            result))
        ((thunk? obj)
          (actual-value (thunk-exp obj) (thunk-env obj)))
        ((evaluated-thunk? obj)
          (thunk-value obj))
        (t obj)))

Now, the main change is done only in apply.:

(defun apply. (proc args env)
  (when (> *evaluator-debug-level* 0)
    (format t "applying ~a to ~a~%" proc args))
  (cond ((primitive-procedure? proc)
          (apply-primitive-procedure 
            proc 
            (list-of-arg-values args env)))
        ((compound-procedure? proc)
          (eval-sequence
            (procedure-body proc)
            (setup-arguments-env              
              (procedure-parameters proc)
              args
              env
              (procedure-env proc))))
        (t
          (error
            "Unknown procedure type in APPLY: " proc))))

The difference here is in the call to eval-sequence in case of a compound procedure. Instead of directly calling extend-environment with a list of delayed arguments, I’m calling a special resolution function that will treat all arguments appropriately. Here it is:

(defun setup-arguments-env (operands args args-env proc-env)
  (let ((vars '())
        (vals '()))
    (loop 
      for op in operands
      for arg in args
      do
        (cond ((lazy-memo-operand? op)
                (push (lazy-operand-name op) vars)
                (push (delay-it-memo arg args-env) vals))
              ((lazy-operand? op)
                (push (lazy-operand-name op) vars)
                (push (delay-it arg args-env) vals))
              (t ; evaluated-operand?
                (push op vars)
                (push (actual-value arg args-env) vals))))
    (extend-environment vars vals proc-env)))

It “pattern matches” the list of operands the of the procedure with the list of actual arguments, and builds up a list of values to extend this environment. For some arguments the values are computed directly, and for some thunks (or memoized thunks) are placed in the list.

Note that I’m not changing eval. at all. This is a design decision that could have been different. I decided to resolve the procedure parameter list at the very last moment (when the procedure is applied). This is not the most efficient way to do this (the resolution can be done at definition time) but it’s the one requiring the least amount of code. Figuring out such a way for yourself is a great boost to the understanding of the evaluator’s code structure – which is exactly why I recommend trying this exercise on your own.

1 Perhaps this is not a perfect example because CL’s unless is a bit different from the one proposed in this chapter. Read about it here.

2 print is just like display for the sake of our discussion – a primitive procedure.


Comments

comments powered by Disqus