CS 2135: Programming Language Concepts
Notes on Delayed Substitution

This page summarizes the lecture on delayed substitution.


Motivation

Our original interpreter (eval) handles function calls by substituting arguments for parameters in the body of the function. Consider the following Curly expression:

{{proc {x}
   {{proc {y}
      {{proc {z}
         {return x + y + z}}
       3}}
    4}}
 5}

How many times will subst traverse {return x + y + z} while evaluating this expression? Three -- once for each function call. How many times will {return x + y + z} be evaluated? Once -- when the final substitution (for z) is finished.

This suggests an inefficiency in our interpreter: we substitute more often than we evaluate. We could improve on this situation by delaying substitutions until a variable is actually encountered during evaluation. Implementing this requires an additional data structure that associates variables with their values:

(define-struct dsub (var value))

It also requires that we change the contract on interp to take a list of substitutions as an argument.

interp : expr list[dsub] -> value 

A list of dsubs is called an environment.

Handing dsubs requires two changes to interp: the symbol? case must lookup values in the environment, and somewhere we must add new dsubs to the environment. The original interpreter code performed subst in the call? case, so it makes sense that we would create new dsubs in this case. Specifically, the call? case must add a new dsub to the environment before processing the body.

Making these two changes, but leaving the rest of the interpreter alone yields our first version of a dsub interpreter.

This new interpreter should yield the same answers as running the original interpreter. Consider the following expression:

(make-call
 (make-proc 'x (make-call
		(make-proc 'f (make-call
			       (make-proc 'x (make-call 'f 10))
			       5))
		(make-proc 'y (make-add 'x 'y))))
 3)

The interpreter with subst yields 13, but the new interpreter returns 15. What's wrong?

The dsub interpreter looks up variable values in the current environment. This means that when a variable is encountered, its value is taken to be the mos recent value for that variable. But this is dynamic scoping! Our interpreter is supposed to implement static scoping. Somehow, we've changed the scoping rules with the use of delayed substitutions.

To see the problem more clearly, let's play stepper on the dsub interpreter:

  1. (interp-d
     (make-call
      (make-proc 'x (make-call
    		 (make-proc 'f (make-call
    				(make-proc 'x (make-call 'f 10))
    				5))
    		 (make-proc 'y (make-add 'x 'y))))
      3)
     empty)
    
  2. (interp-d
     (make-call
      (make-proc 'f (make-call
    		 (make-proc 'x (make-call 'f 10))
    		 5))
      (make-proc 'y (make-add 'x 'y)))
     (cons (make-dsub 'x 3) empty))
    
    1. [Interpret the argument]
      (interp-d
        (make-proc 'y (make-add 'x 'y))
        (cons (make-dsub 'x 3) empty))
      
    2. [Interpret the function]
      (interp-d
        (make-proc 'f (make-call
      		 (make-proc 'x (make-call 'f 10))
      		 5))
        (cons (make-dsub 'x 3) empty))
      
  3. (interp-d
     (make-call (make-proc 'x (make-call 'f 10))
    	    5)
     (cons (make-dsub 'f (make-proc 'y (make-add 'x 'y)))
           (cons (make-dsub 'x 3)
    	     empty)))
    
  4. (interp-d
     (make-call 'f 10)
     (cons (make-dsub 'x 5)
           (cons (make-dsub 'f (make-proc 'y (make-add 'x 'y)))
    	     (cons (make-dsub 'x 3)
    		   empty))))
    
  5. (interp-d
     (make-call (make-proc 'y (make-add 'x 'y)) 10)
     (cons (make-dsub 'x 5)
           (cons (make-dsub 'f (make-proc 'y (make-add 'x 'y)))
    	     (cons (make-dsub 'x 3)
    		   empty))))
    
  6. (interp-d
     (make-add 'x 'y)
     (cons (make-dsub 'y 10)
           (cons (make-dsub 'x 5)
    	     (cons (make-dsub 'f (make-proc 'y (make-add 'x 'y)))
    		   (cons (make-dsub 'x 3)
    			 empty)))))
    
  7. (interp-d
     (make-add 5 10)
     (cons (make-dsub 'y 10)
           (cons (make-dsub 'x 5)
    	     (cons (make-dsub 'f (make-proc 'y (make-add 'x 'y)))
    		   (cons (make-dsub 'x 3)
    			 empty)))))
    
  8. 15
    

Under static scoping, looking up 'x for the make-add should have yielded 3 (the binding of the outer x), but the 5 was more current (ie, it was the dynamically active binding). The code of the current dsub interpreter reveals that it implements dynamic scoping when you compare it to the subst version. Subst goes inside the body of procs to replace variable values, but the dsub interpreter doesn't enter the body of a proc until the proc is called. Thus, in step 1, the subst would replace the 'x in the body of (make-proc 'y (make-add 'x 'y)) when x gets 3. In interp-d, we don't visit the 'x until step 7, by which time we've obscured the statically scoped value of 'x.

Was there a time where we were close to seeing the troublesome 'x when interp-d had the right environment? Yes, in step 2-1. We interpreted ("defined") the proc and the 3 was the most recent value of x in the environment. By static scoping, the environment that interp-d has when it processes a proc declaration is that proc's static environment. Ideally, we need to remember that environment and use it to perform substitution in the body of the proc when the proc is eventually used. Thus, we need a new data structure that associates procs and environments. That data structure is called a closure,

(define-struct closure (proc env))

Interp must change in two places to properly use closures: the proc? case must return a closure instead of a proc, and the call? case will now get a closure, rather than a proc, as the call-func value. See the corrected interpreter code for the details.

Closures are one of the most important concepts in this course. Whenever you have the ability to nest function or (object-oriented) class definitions, you need closures to get static scoping. When a language claims to provide "first-class functions" or "first-class classes" (meaning functions and classes that can be returned from and passed to other functions, stored in data structures, etc), make sure it implements a form of closures. Otherwise, you will have to construct the closures by hand to achieve static scoping.


Questions


This page maintained by Kathi Fisler
Department of Computer Science Worcester Polytechnic Institute