Lecture 19: Recursion


Why Recursion Doesn't Work Yet

Consider the following example:

let f = proc (x) 
          if x then (f -(x,1)) else 0
		in (f 3)

Shouldn't this work? Try typing it into the interpreter from the last lecture and see for yourself that it doesn't.

The reason this doesn't work is that the expression proc (x) if x then (f -(x,1)) else 0 is evaluated before the variable f is created. This doesn't directly cause a problem when you define the function, but consider what happens when you call the function f:

  1. The environment stored in the closure is extended by binding the formal parameters to their actual values.
  2. The body of the function is evaluated in the new environment.

When we get to the recursive call to f, we look in the environment to see what f is. It's not one of the formal parameters, so we look in the environment our current environment is an extension of--i.e., the environment stored in the closure. But the environment stored in the closure is the environment before the new environment for the let is created (the one containing f). So, no f is found there!

This situation looks like this:

Remember, Scheme's own let expression works the same way and has the same limitation. Scheme defines a letrec expression that has to work a little harder to make recursion work.

The key to making recursion work is that the closures for recursive functions have to refer to the environment in which the function itself is defined. This recursive situation looks like this:

Let's implement our own version of something like Scheme's letrec.


Syntax

Here's the syntax we'll use to define recursive functions. Notice that we define the name, the formal parameters, and the body at the same time.

The BNF for <expression> (with recursive functions)

<expression> ::= letrec {<identifier> ( {<identifier>}*(,) ) = <expression> }* in <expression>
 

Again, the notation "{ ... }*(,)" means zero or more occurrences separated by commas.

For example,

letrec
   fact(x) = if zero?(x) then 1 else *(x,(fact sub1(x)))
in (fact 6)

or

letrec
   even(x) = if zero?(x) then 1 else (odd  sub1(x))
    odd(x) = if zero?(x) then 0 else (even sub1(x))
in (odd 13)

The abstract syntax record (variant of expression) returned when a letrec is parsed is

(letrec-exp
  (procnames (list-of symbol?))
  (idss (list-of (list-of symbol?)))
  (bodies (list-of expression?))
  (letrec-body expression?))

Behavior

The key to making recursion work is to build the closures so that they store the environment created by the letrec, not the preceding environment. To do this, we'll have to use a slightly different form of environment. In particular, let's assume we have a function in the environment handling code that looks like this:

(define extend-env-recursively
   (lambda (proc-names idss bodies env)
      ...
	  ))

Unlike the normal extend-env, to which you just pass variable names and values, extend-env-recursively takes the unevaluated components that make up the procedure definitions: the formal parameters for each function (the idss list of lists) and the bodies for each of the functions (the bodies list). It then builds the necessary closures and binds them to the newly created environment. The result looks something like the second figure above.

The new case in eval-expression for letrec-exp thus looks a lot like the one for let-exp, except that we use this new recursive-environment builder to build the new local environment:

(define eval-expression 
  (lambda (exp env)
    (cases expression exp
      (lit-exp (datum) datum)
      (var-exp (id) (apply-env env id))
      (primapp-exp (prim rands)
        (let ((args (eval-rands rands env)))
          (apply-primitive prim args)))
      (if-exp (test-exp true-exp false-exp)
        (if (true-value? (eval-expression test-exp env))
          (eval-expression true-exp env)
          (eval-expression false-exp env)))
      (let-exp (ids rands body)
        (let ((args (eval-rands rands env)))
          (eval-expression body (extend-env ids args env))))
      (proc-exp (ids body) (closure ids body env))
      (app-exp (rator rands)
        (let ((proc (eval-expression rator env))
              (args (eval-rands rands env)))
          (if (procval? proc)
            (apply-procval proc args)
            (eopl:error 'eval-expression
              "Attempt to apply non-procedure ~s" proc))))
      ;; new case for letrec-exp
      (letrec-exp (proc-names idss bodies letrec-body)
        (eval-expression letrec-body
          (extend-env-recursively proc-names idss bodies env)))     
      )))

Changes to the Environments

The main change that we'll make to cause recursion to work is to implement this new extend-env-recursively function. Your textbook describes a few ways to do it, depending on how environments are implemented. There are two main variations in these approaches, regardless of the data structures used:

  1. Build the closures on-the-fly as the environment is applied. (Since this is after the environment is created, the new closures can refer back to the environment recursively.) This is simple to implement but has the drawback that every time you call the function you have to create the closure on-the-fly.
  2. Build the environment first without putting in the closures yet, then build the closures, then modify the environment to permanently store the recursive closures.

We'll take this latter approach.

First, we'll build the environments using the implementation from Lecture 12 that used defined datatypes:

(define-datatype environment environment? 
  (empty-env-record)             
  (extended-env-record
    (syms (list-of symbol?))
    (vals vector?)
    (env environment?)))

Notice one change from the implementation in Lecture 12: instead of storing the values (vals) in a list, we'll use a vector for random access.

The apply-env function is the same as before (just using vector instead of list operations on vals):

(define apply-env
  (lambda (env sym)
    (cases environment env
      (empty-env-record ()
        (eopl:error 'empty-env "No binding for ~s" sym))
      (extended-env-record (syms vals old-env)
        (let ((pos (rib-find-position sym syms)))
          (if (number? pos)
            (vector-ref vals pos)
            (apply-env old-env sym)))))))

The constructor for empty environments is the same:

(define empty-env
  (lambda ()
    (empty-env-record)))

When we extend an environment, the process is also the same--we create a new extended-env-record:

(define extend-env
  (lambda (syms vals env)
    (extended-env-record syms (list->vector vals) env)))

These first three functions are the ones we implemented previously (slightly modified to use a vector for the values (vals)). Now, we need to implement the extend-env-recursively function:

(define extend-env-recursively
  (lambda (proc-names idss bodies old-env)
    (let ((len (length proc-names)))
      (let ((vec (make-vector len)))
        (let ((env (extended-env-record
                     proc-names vec old-env)))
          (for-each
            (lambda (pos ids body)
              (vector-set! vec pos (closure ids body env)))
            (iota len) idss bodies)
          env)))))

Here is where we implement the main changes required to make recursion work. We use the make-vector function to make a vector the same length as the number of procedures we're defining (the proc-names list), but we don't put anything in it just yet. We now build the new environment (extended-env-record) and store it as a local variable. We can now create the closures and store the new environment in them. After we do this, we can use vector-set! to change the contents of the vals vector to contain the new closures. Thus, the environment stores the closures, and each closure stores a reference back to the environment--the essential component in making recursive functions work.

Finally, a few helper functions used by these functions (you don't really need to know how these work):

Here are their implementations:

(define rib-find-position
  (lambda (sym los)
    (list-find-position sym los)))
 
(define list-find-position
  (lambda (sym los)
    (list-index (lambda (sym1) (eqv? sym1 sym)) los)))
 
(define list-index
  (lambda (pred ls)
    (cond
      ((null? ls) #f)
      ((pred (car ls)) 0)
      (else (let ((list-index-r (list-index pred (cdr ls))))
              (if (number? list-index-r)
                (+ list-index-r 1)
                #f))))))
 
(define iota
  (lambda (end)
    (let loop ((next 0))
      (if (>= next end) '()
        (cons next (loop (+ 1 next)))))))

The complete interpreter for Lecture 19.



Last updated at 9:23 am on Friday, July 15, 2005.