Recitation #8: Lazy/Eager evaluation, thunks

Written by Alan Shieh

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