CS312 Lecture 19: Dynamic scoping, normal order

Today's topic: modifying the evaluator.

Static scoping

Consider a program that calls a function foo. How does the caller communicate with the callee? In ML, and in the languages we have looked at to date, communication only happens via the arguments. This is an essential part of modularity.

Consider the following code:

let
  val x:int = 3 
  fun foo(u: int, v: int): int = u + x 
  val x:int = 5 
in 
    foo(4, 42) 
end

In this code, foo adds 3 to its first argument. The result is 7. Recall the static scoping rule (substitution model, and evaluator): to find the binding occurrence for a variable reference, just look up, textually (sometimes called lexical scoping).

How can the caller of foo change foo's behavior? Only by changing its arguments. Corollary: if foo has no arguments, the called can't ever change its behavior.

Suppose that you want to change foo's behavior without changing its arguments? Example: print-depth, or similar debugging information.

In a statically scoped language, you can't do this. In fact, the language is designed not to allow this!

Here is the relevant part of the evaluator (this is not precisely how the code actually looks):

evaluateApply (e1: exp, e2: exp, encrt: env): value * typ =
    let
      val (fc, fct)  = evaluate(e1, encrt)
    in
      case fc of
        SpecForm_v(name)            => specialForm(name, e2, encrt)
      | Predef_v  (name)            => predefined (name, evaluate (e2, encrt))
      | Fn_v(fal, clenv, body, name)  =>
         let
          val en = clenv
         in
           evaluate(body, addbindings(fal,evaluate(e2,encrt), en))
         end

Note that the environment in which we were called, encrt, is not the environment we use to evaluate the function body. Once we are done evaluating e2, encrt is dropped on the floor.

Instead, we use the environment clenv from the closure (result of evaluating e1). This is worth going over in detail!

Consider evaluating (fn(x) => x + 1) (2+3) in some env E1.

Consider evaluating (let val z:int = 1 in fn(x) => x + z end) (2+3) in some env E1.

Dynamic scoping

What if we want to change our language, so that

let
  val x:int = 3 
  fun foo(u: int, v: int): int = u + x 
  val x:int = 5 
in 
    foo(4, 42) 
end
evaluates to 9 instead of 7? I.e., to look up the value of a variable, you find the binding in the environment in which you are called, rather than lexically? This is called dynamic scoping, and historically was widely used in the 1960's.

How do we change this?

evaluateApply (e1: exp, e2: exp, encrt: env): value * typ =
    let
      val (fc, fct)  = evaluate(e1, encrt)
    in
      case fc of
        SpecForm_v(name)            => specialForm(name, e2, encrt)
      | Predef_v  (name)            => predefined (name, evaluate (e2, encrt))
      | Fn_v(fal, clenv, body, name)  =>
         let
          val en = encrt (* only change *)
         in
           evaluate(body, addbindings(fal,evaluate(e2,encrt), en))
         end

Important lesson: small changes in the evaluator lead to large changes in the language.

Prelim #2 preview: here is a small change to the evaluator; what does it now do? What is the new value of the following expression?

Note that with dynamic scoping there is no real point to closures, since their environment clenv is discarded.

In fact, we can make our evaluator more general:

evaluateApply (e1: exp, e2: exp, encrt: env): value * typ =
  (
    let
      val (fc, fct)  = evaluate(e1, encrt)
    in
      case fc of
        SpecForm_v(name)            => specialForm(name, e2, encrt)
      | Predef_v  (name)            => predefined (name, evaluate (e2, encrt))
      | Fn_v(fal, clenv, body, name)  =>
         let
          val en = case SCOPING of 
                  STATIC => clenv
                | DYNAMIC => encrt
         in
           evaluate(body, addbindings(fal,evaluate(e2,encrt), en))
         end

OK, what else can we do? Consider how we go about defining functions. When we say something like

fun sqr(z:int):int = z*z

our intent is that everywhere we write sqr(WHATEVER) we might as well have written WHATEVER*WHATEVER.

This concept is known as referential transparency, and is the key to designing large programs. Functional languages come pretty close to doing this.

However, they don't quite do it. For example consider the following example:

let
  val x:int = 3 
  fun foo(u: int, v: int): int = u + x 
in 
    foo(4, raise Fail "banana") 
end
To solve this issues we will introduce "near" ML languages that use a lazy evaluation instead of an eager evaluation (refer to next lecture).

CS312  © 2002 Cornell University Computer Science