Please also see Substitution Rules handout.
Lazy vs. Eager evaluation
If expression
Now that we have seen more formal machinery, let's revisit the evaluation order example that we went over in section earlier in the semester. Recall that we tried to re-implement the if expression as a function:
fun my_if(c,e1,e2) = if c then e1 else e2
my_if doesn't work as a replacement for if because the expressions e1 and e2 are always evaluated. Hence, if the condition guards against an exception or infinite loop in one branch, then my_if cannot replace if:
fun F(x) = if false then F(x) else true val rval = F(0)
returns true as expected, and evaluates as follows:
eval(F(0)) E3.0 eval(F) = fn x => if false then F(x) else true E3.1 eval(0) = 0 E3.2 substitute([(x,0)], if false then F(x) else true) = if false then F(0) else true E3.3 eval(if false then F(0) else true) E6.0 eval(false) = false E6.2 if false = false then v' = eval(true) eval(if false then F(0) else true) = true
However, my_if enters an infinite loop:
fun F(x) = my_if(false, F(x), true) val rval = F(0)
evaluates as
eval(F(0)) E3.0 eval(F) = fn x => my_if(false, F(x), true) E3.1 eval(0) = 0 E3.2 substitute([(x,0)], my_if(false, F(x), true)) = my_if(false, F(0), true) E3.3 eval(my_if(false, F(0), true)) E3.0 eval(my_if) = fn x => if c then e1 else e2 E3.1 eval((false,F(0),true)) (*) eval(false) = false eval(F(0)) = E3.0 eval(F) = fn x => my_if(false, F(x), true) E3.1 eval(0) = 0 E3.2 substitute([(x,0)], my_if(false, F(x), true)) = my_if(false, F(0), true) E3.3 eval(my_if(false, F(0), true)) E3.0 eval(my_if) = fn x => if c then e1 else e2 E3.1 eval((false,F(0),true)) = (*) ...
Note the loop, denoted by (*)
However, if SML had lazy evaluation, the recursive call to F is evaluated only if needed: (Note that the lazy version of E3 is used)
eval(F(0)) E3.0 eval(F) = fn x => my_if(false, F(x), true) E3.1 substitute([(x,0)], my_if(false, F(x), true)) = my_if(false, F(0), true) E3.2 eval(my_if(false, F(0), true)) E3.0 eval(my_if) = fn x => if c then e1 else e2 E3.1 substitute([(c,false), (e1,F(0)), (e2,true)], if c then e1 else e2) = if false then F(0) else true E3.2 eval(if false then F(0) else true) E6.0 eval(false) = false E6.2 if false = false then v' = eval(true) eval(if false then F(0) else true) = true
This example shows that lazy evaluation is useful in implementing special primitives, especially those dealing with control flow.
What are the pros and cons of lazy evaluation and eager evaluation. Lazy evaluation provides slightly more power to functions. As shown above, we can use lazy evaluation to implement if expressions with the desired semantics. andalso and orelse can be implemented in a similar fashion.
Lazy evaluation evaluates arguments only when needed, hence potentially improves performance. However, lazy evaluation gets into trouble if a parameter is used repeatedly in a function, and the actual value is needed multiple times. A naive runtime would evaluate the parameter multiple times. A more sophisticated runtime might cache the result value, at the cost of added complexity. Lazy evaluation also complicates expressions with side effects (especially when caching is involved).
Eager evaluation is more popular than lazy evaluation in modern languages. This is because evaluation order and performance implications are easier for the programmer to understand.
"Lazy" evaluation with first class function values
There are some abstractions where lazy evaluation is useful (e.g., to represent an infinite list). It would be nice if we could capture the notion of lazy evaluation when needed, even when the language does not directly support lazy evaluation. It turns out that having functions as first class values allows us to implement this functionality in a straightforward manner.
Recall that free variables in function declarations (that is, those variables that do not occur in the parameter list or inside any local variable bindings) are subsituted with the bindings that were active at the point of declaration:
val g = let val x = 3 in fn () => x * x end val x = 4 g() == > 9 (* for value of g, value of x is still 3 *)
In general, an expression e can be "frozen" as an anonymous function fn() => e. We can force (evaluate) the expression by applying the function value. Let's modify my_if with this trick:
fun my_if(c,e1,e2) = if c then e1() (* evaluate e1 *) else e2()
fun F(x) = my_if(false, fn() => F(x), fn() => true)
Evaluating, we get
eval(F(0)) E3.0 eval(F) = fn x => my_if(false, fn() => F(x), fn() => true) E3.1 eval(0) = 0 E3.2 substitute([(x,0)], my_if(false, fn() => F(x), fn() => true)) = my_if(false, fn() => F(0), fn() => true) E3.3 eval(my_if(false, fn() => F(0), fn() => true)) E3.0 eval(my_if) = fn x => if c then e1() else e2() E3.1 eval((false,F(0),true)) (*) eval(false) = false eval(fn() => F(0)) = fn() => F(0) eval(fn() => true) = fn() => true = (false, fn() => F(0), fn() => true) E3.2 substitute([(c,false), (e1, fn() => F(0)), (e2, fn() => true)], if c then e1() else e2()) = if false then (fn() => F(0))() else (fn() => true)() E3.3 eval(if false then (fn() => F(0))() else (fn() => true)()) E6.0 eval(false) = false E6.2 if false = false then v' = eval((fn() => true)())) ... v' = true eval(if false then (fn() => F(0))() else (fn() => true)()) = true
This technique is known as thunking, and the anonymous functions are known as thunks. Indeed, some languages implement lazy evaluation by transforming parameters into thunks.
More fold examples
DFA
See previous notes.
Unzip
fun ('a,'b) unzip(pairlist: ('a * 'b) list): ('a list * 'b list) = let val (al:'a list,bl: 'b list) = foldl (fn((a:'a,b:'b), (al:'a list, bl:'b list)) => (a::al, b::bl)) ([],[]) pairlist in (rev al, rev bl) end
Power set
Use the mathematical fact that one can think of the powerset of B=A union {x} as containing the subsets of B that do not include x (e.g. the powerset of A ), and the subsets of B that do include x (e.g. the sets in the powerset of A, but with x added).
fun 'a powerset(al:'a list): 'a list list = (* Outer fold generates powerset for successively larger subsets of al *) foldl (fn (x,acc) => (* Inner fold is a way of saying (map (fn(y) x::y) acc) @ acc * without the @ *) foldl (fn (y,acc1) => (x::y)::acc1) acc acc) [[]] al