SICP 7b
Metacircular Evaluation II

Table of Contents

Metacircular interpreters are convenient for exploring language issues and excellent for exchanging ideas about language design.

1 Indefinite number of arguments

One potentially helpful feature to add to a language, is allowing procedures to take an indefinite number of arguments.

(+ 3 5 7 5 3)

To add this feature, we require a syntactic specification to notate the additional arguments.

(lambda (x . y)
  (map (lambda (u) (+ x u))
       y))

.: syntax for representing cons​es.

Note the syntactic difference between these three expressions.

(lambda (x y z) ...)
(lambda (x . y) ...)
(lambda x x)

Note (lambda x x) is equivalent to list.

Now incorporate this feature into the interpreter. For this we need to change pair-up.

(define pair-up
  (lambda (vars vals)
    (cond
     ((eq? vars '())
      (cond ((eq? vals '()) '())
            (else (error "too many arguments"))))
     ((symbol? vars)               ;new
      (cons (cons vars vals) '())) ;new
     ((eq? vals '()) (error "too few arguments"))
     (else
      (cons (cons (car vars)
                  (cdr vals))
            (pair-up (cdr vars)
                     (cdr vals)))))))

2 Dynamic binding of variables

Next, we will attempt a more substational variation: dynamic binding of variables. First, we'll observe why it may be useful to have.

(define sum
  (lambda (term a next b)
    (cond ((> a b) 0)
          (else
           (+ (term a)
              (sum term
                   (next a)
                   next
                   b))))))
(define sum-powers
  (lambda (a b n)
    (sum (lambda (x) (expt x n))
         a
         1+
         b)))
(define product-powers
  (lambda (a b n)
   (product (lambda (x) (expt x n))
            a
            1+
            b)))

Notice that sum-powers and product-powers are essentially the same, which is a sure indicator something is awry.

(define sum-powers
  (lambda (a b n)
    (sum nth-power a 1+ b)))
(define product-powers
  (lambda (a b n)
    (product nth-power a 1+ b)))
(define nth-power
  (lambda (x)
    (expt x n)))

Upon first glance, it appears this is preferable. However this is currently impossible, as n remains undefined.

Dynamic binding: a free variable in a procedure has its value defined in the chain of callers, rather than where the procedure is defined.

(define eval
  (lambda (exp env)
    (cond ((number? exp) exp)
          ((symbol? exp) (lookup exp env))
          ((eq? (car exp) 'quote) (cadr exp))
          ((eq? (car exp) 'lambda) exp)   ;new
          ((eq? (car exp) 'cond)
           (evcond (cdr exp) env))
          (else (apply (eval (car exp) env)
                       (evlist (cdr exp) env)
                       env)))))           ;new

We no longer have to save the environment upon definition of the procedure so no closure is required. However, the environment is required upon the application of the procedure.

(define apply
  (lambda (proc args env) ;new
    (cond
     ((primitive? proc)
      (apply-primop proc args))
     ((eq? (car proc) 'lambda)
      ;; proc = (LAMBDA bvis body)
      (eval (caddr proc) ;body
            (bind (cadr proc) ;bvis
                  args
                  env))) ;new
     (else error-unknown-procedure))))

The catch is that dynamic binding leads to a modularity crisis. For example, if next in sum were to be renamed n, then sum-powers would stop working. Dynamic binding removes the ability to have qualifiers.

So what would be the correct solution? How do we gain the power of dynamic binding within a lexical system?

(define pgen
  (lambda (n)
    (lambda (x) (expt x n))))

(define sum-pairs
  (lambda (a b n)
    (sum (pgen n) a 1+ b)))
(define product-pairs
  (lambda (a b n)
    (product (pgen n) a 1+ b)))

3 Delayed parameters

Finally, we will add the ability to delay the calculation of parameters.

(define (unless p c a)
  (cond ((not p) c)
        (else a)))

Without delayed parameters, the following code can't be run, as evaluating the arguments leads to an error.

(unless (= 1 0) 2 (/ 1 0))

What we would like instead, is for the following procedure to be constructed.

(cond ((not (= 1 0)) 2)
      (else (/ 1 0)))

To construct this, build a kluge.

(define (unless p (name c) (name a))
  (cond ((not p) c)
        (else a)))

Adding support for delayed parameters requires a far more complicated modification of the interpreter compared to adding both of the previous features.

(define eval
  (lambda (exp env)
    (cond ((number? exp) exp)
          ((symbol? exp) (lookup exp env))
          ((eq? (car exp) 'quote) (cadr exp))
          ((eq? (car exp) 'lambda)
           (list 'closure (cdr exp) env))
          ((eq? (car exp) 'cond)
           (evcond (cdr exp) env))
          (else
           (apply (undelay (eval (car exp) ;new
                                 env))
                  (cdr exp)
                  env)))))

(define apply
  (lambda (proc args env) ;new
    (cond
     ((primitive? proc)
      (apply-primop proc
                    (evlist ops env))) ;new
     ((eq? (car proc) 'closure)
      ;; proc = (CLOSURE (bvrs body) env
      (eval (cadadr proc)
            (bind (vnames (caadr proc)) ;new
                  (gevlist (caadr args) ;new
                           ops
                           env)
                  (caddr proc))))
     (else error-unknown-procedure))))

(define evlist
  (lambda (l env)
    (cond
     ((eq? l '()) '())
     (else
      (cons (undelay (eval (car l) env))
            (evlist (cdr l) env))))))

(define gevlist
  (lambda (vars exps env)
    (cond
     ((eq? exps '()) '())
     ((symbol? (car vars)) ;applicative
      (cons (eval (car exps) env)
            (gevlist (cdr vars)
                     (cdr exps)
                     env)))
     ((eq? (caar vars) 'name)
      (cons (make-delay (car exps) env)
            (gevlist (cdr vars)
                     (cdr exps)
                     env)))
     (else error-unknown-declaration))))

(define evcond
  (lambda (clauses env)
    (cond
     ((eq? clauses '()) '()) ;arbitrary
     ((eq? (caar clauses) 'else)
      (eval (cadar clauses) env))
     ((false? (undelay
               (eval (caar clauses)
                     env)))
      (vcond (cdr clauses) env))
     (else
      (eval (cadar clauses) env)))))

(define false?
  (lambda (x) (eq? x nil)))

(define make-delay
  (lambda (exp env)
    (cons 'thunk (cons exp env))))

(define (undelay v)
  (cond ((pair? v)
         (cond ((eq? (car v) 'thunk)
                (undelay
                 (eval (cadr v)
                       (cddr v))))
               (else v)))
        (else v)))

Thunk apparently is the sound of hitting a stack.