CS312 Lecture 20: Normal order, lazy evaluation & streams

Administrivia

Normal order evaluation

Consider the examples:

let
  val x:int = 3 
  fun foo(u: int, v: int): int = u + x 
in 
    foo(4, raise Fail "banana") 
end

fun factorial2 (n : int) : int =
  my_if (n <= 0, 1, n * factorial2 (n - 1))
To allow code like this to work, we need a language "near" ML, called normal order (sometimes called "lazy").

ML we have used until now is considered eager/applicative order/strict language. Some other functional languages like Haskell are lazy/normal order/non-strict. We say that a function F is strict in an argument arg if F diverges when it's called with an argument arg that diverges.

What about mini-ML? We can make it be anything we want! How do we change it to do this? The trick is always in the function evaluateApply:

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 (* static scoping *)
         in
           evaluate(body, addbindings(fal,evaluate(e2,encrt), en))
         end

We need to delay the evaluation of e2, until we really need it. Note, however, that when we go to evaluate e2, we need more information than just e2; we also need the evironment encrt!

So we need something that "packages up" something to evaluate with an environment. We already have this and it's called a closure.

In fact, normal order can be (sort of) "faked" in an applicative order language. We simply do:

fun if_funs (b:bool, t:unit->'a, f:unit->'a) : 'a=
  if b then t() else f()

fun factorial3 (n : int) :int =
  if_funs (n <= 0, fn () => 1, fn () => n * factorial3 (n - 1))

This "packaging up" of an expression to evaluate and an environment to evaluate it in, is called a thunk. The process of making one is called "thunking", and the process of getting the value from a thunk is called forcing the thunk.

In order to make mini-ML into normal order, we need to do thunking during evaluateApply. We create a thunk that contains the arguments to be evaluated, and the environment in which to evaluate them. This is the only place where we create a thunk?

There is only one operation on a thunk, which is to force it. When do we force a thunk? When we actually need to know its value (our laziness has runs its course). This will happen in primitives or conditionals. For example, a unary or binary operation will need to force both arguments. [Note that this is not, of course, completely necessary; consider 0*foo(). But in our mini-ML, we do type checking dynamically, so we need it]

Also, if will need to force its first argument, and to conditionally force its 2nd or 3rd arguments.

Where else do we need to force a thunk? Answer: at top level!

What is inefficient about this? Suppose that we use an argument many times.

fun foo(x:int):int = x + (x*x*x) - bar(x);
foo(fact(100));
This is actually one reason why normal order isn't used much in practice.

We can't fix this in a functional setting; we need side effects (refer to next lecture).

What else can we do in a normal order language? Some pretty crazy things!

Streams

You can have recursive functions that delay their calls, like factorial. For instance, consider:

fun Cons(n:int,l:int list):int list = n::l;
fun nats(n:int):int list = Cons(n,nats(n+1));
nats(0);
The call to nats(0) will create a list of all natural numbers, we can't do this in applicative order. But we do in normal order.

Note: we have to introduce the definition of Cons in order to have normal order to delay the evaluation of nats(n+1). This representation of natural numbers is called a stream, and we will only know the next value of the secuence when we ask for the next element of the list. Streams can be very useful, for instance if you need to have programs that return several answers to a problem.

Question: what goes wrong with this, even in normal mini-ML?


CS312  © 2002 Cornell University Computer Science