Lecture 15: Lazy evaluation and thunks

Administrivia

Prelim #1 returned in section. Average = 71.89, high = 99. Probably a smidge too long, but overall people did well.

Normal order evaluation

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 ford(u: int, v: int): int = u + x 
in 
    ford(4, raise Fail "banana") 
end

To solve this issues we will introduce "near" ML languages that use lazy evaluation instead of eager evaluation.

The key notion is a thunk, which is basically a promise to evaluate some expression later on (when it is needed). This is referred to as forcing a thunk. A thunk will be a new kind of value.

Thunks will be created when we apply a function to its arguments. We will create thunks for all the arguments, promising to evaluate them (in the current environment) later on, when and if they are actually needed.

When are thunks forced? Well, when their value is actually needed. But that is actually a tricky question.

Some cases are simple. if, for example, or andalso or orelse, force one argument and depending on its value force some others. Simple primitives, like +, force both/all their arguments.

But there are some real subtleties here.

Consider the expression (fn(x)=>x)(42). This will give us (what?) a thunk. So clearly the top-level loop needs to force its arguments.

Now consider the expression (fn(x)=>x) ((fn(y)=>y)(42)). What happens when we force the thunk? We get another thunk, unless we are careful.

The solution is to define forcing a thunk to force its value repeatedly, until we get a (non-thunk) value.

Also, what about function application? When we see e1(e2) we clearly need to force e1, otherwise we have no idea what to do with e2.

Evaluator changes for lazy evaluation

  datatype value =
    Int_v      of int
  | Fn_v       of (string option     (* function name        *)
                   * exn             (* environment          *)
                   * string list     (* names of arguments   *)
                   * exp)            (* body                 *)
  | Predef_v   of string      (* name of predefined function *)
  | SpecForm_v   of string      (* name of predefined function *)
  | Thunk_v    of exp * env         (* for lazy evaluation  *)
 

evaluateApply(func: exp, arg: exp, env: env) =
  let
        val f = forceValue(evaluate(func, env))
        fun evalAsUsual() =
          let
            val arglist = (* evaluate(arg, env) *)
              case arg of
                Tuple_e alist => 
                   map (fn(ex:exp)=>Thunk_v(ex,env)) alist
              | Unit_c => [Unit_v]
              | _ => [Thunk_v(arg,env)]
          in
            case f of
              Fn_v(_) => apply(f,arglist)
            | Predef_v (name) => applyPredef(name,arglist)
            | _ => Error.runtime "attempted to apply non-function"
          end

        fun evalSpecial(name: string) =
          case (name,arg) of
            ("if3", Tuple_e [arg1, arg2, arg3]) =>
              (case evaluate(arg1,env) of
                 Bool_v true => evaluate(arg2, env)
               | Bool_v false => evaluate(arg3, env))
          | _ => Error.runtime "no handler for this special form"
    in
      case f of
        SpecForm_v(name) => evalSpecial(name)
        | _ => evalAsUsual()
    end
 
 

Then we also have to insert a zillion calls to forceValue in the right places.

Once we are done with this we have a working lazy evaluator (a contradiction in terms?) Now we actually don’t need if3 as a special form, we could just write it as a function!

Streams

OK, let us now turn to lists . Should :: be lazy or not?