Prelim #1 returned in section. Average = 71.89, high = 99. Probably a smidge too long, but overall people did well.
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.
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()
endThen 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!
OK, let us now turn to lists . Should :: be lazy or not?