CSCI 2041

ADVANCED PROGRAMMING PRINCIPLES

Lazy Evaluation:

Using Lazy Computations

Evaluating expressions

evaluating

(take 2 (range 0 3)) ≡ (take @@ 2) @@ ((range @@ 0) @@ 3)

by value:

(take 2 (0::(range 1 3)))
(take 2 (0::(1::(range 2 3))))
(take 2 (0::1::2::(range 3 3)))
(take 2 (0::1::2::[]))
0::(take 1 1::2::[])
0::1::(take 0 2::[])
0::1::[]

by name:

(take 2 (0::(range 1 3)))
0::(take 1 (range 1 3))
0::(take 1 (1::(range 2 3)))
0::1::(take 0 (range 2 3))
0::1::[]

Evaluation stops when a normal form, or value is reached:

  • A constant belonging to a built-in type; or
  • A constructor applied to the correct number of arguments (in normal form); or
  • A function applied to no arguments (i.e. fun x -> e)

Call-by-value is also called applicative order or eager evaluation, and is the more common eval rule. (used in Python, Java, C/C++, OCaml,…)

Call-by-name is also called lazy or normal-order evaluation.

The rules can lead to different behavior, e.g. …

let rec flip x y = flip y x in
let ignore z = "ignored" in
  (ignore (flip 100 200))
let cdec x  = if x > 64 then x-1 else x
let v = cdec (fib 40)

Does call-by-name always use fewer evaluations?

let cdec x = if x > 64 then x-1 else x
let v = cdec (fib 40)

Call-by-name will evaluate (fib 40) twice.

We can avoid this using expression sharing, where expressions are graphs rather than trees.

Call-by-name with sharing is also called call-by-need or lazy evaluation.

lazyCaml

Imagine programming in lazyCaml:

type 'a tree = Empty | Leaf of 'a | Node of ('a tree) * ('a tree)

let rec dfs t = match t with
| Empty -> []
| Leaf x -> [x]
| Node (lt,rt) -> (dfs lt) @ (dfs rt)

Suppose we want to decide if two trees result in the same depth-first search:

let rec listeq l1 l2 = match (l1,l2) with
 | ([],[]) -> true
 | ((h1::t1), (h2::t2)) -> h1=h2 && (listeq t1 t2)
 | _ -> false
let eq_dfs t1 t2 = listeq (dfs t1) (dfs t2)

What happens when we evaluate:

eq_dfs Node(Node(Leaf 3, Leaf 7), Leaf 11) Node(Leaf 7, Node(Leaf 3, Leaf 11))
list_eq (dfs Node(Node(Leaf 3, Leaf 7), Leaf 11)) (dfs Node(Leaf 7, Node(Leaf 3, Leaf 11)))
list_eq ((dfs Node(Leaf 3, Leaf 7)) @ (dfs Leaf 11)) (dfs Node(Leaf 7, Node(Leaf 3, Leaf 11)))
list_eq ((dfs Leaf 3) @ (dfs Leaf 7) @ (dfs Leaf 11)) (dfs Node(Leaf 7, Node(Leaf 3, Leaf 11)))
list_eq 3::((dfs Leaf 7) @ (dfs Leaf 11)) (dfs Node(Leaf 7, Node(Leaf 3, Leaf 11)))
list_eq 3::((dfs Leaf 7) @ (dfs Leaf 11)) ((dfs Leaf 7) @ (dfs Node(Leaf 3, Leaf 11)))
list_eq 3::((dfs Leaf 7) @ (dfs Leaf 11)) 7::(dfs Node(Leaf 3, Leaf 11))
(3=7) && (list_eq ((dfs Leaf 7) @ (dfs Leaf 11)) (dfs Node(Leaf 3, Leaf 11)))
false
eq_dfs Node(Node(Leaf 7, Node(Leaf 3, Leaf 11)),Leaf 2)
       Node(Leaf 7, Node(Leaf 3, Leaf 11))
≡ list_eq (dfs Node(Node(Leaf 7, Node(Leaf 3, Leaf 11)),Leaf 2))
          (dfs Node(Leaf 7, Node(Leaf 3, Leaf 11)))
≡ list_eq ((dfs Node(Leaf 7, Node(Leaf 3, Leaf 11))) @ (dfs Leaf 2))
          (dfs Node(Leaf 7, Node(Leaf 3, Leaf 11)))
≡ list_eq ((dfs Leaf 7) @ (dfs Node(Leaf 3, Leaf 11)) @ (dfs Leaf 2))
          (dfs Node(Leaf 7, Node(Leaf 3, Leaf 11)))
≡ list_eq 7::((dfs Node(Leaf 3, Leaf 11)) @ (dfs Leaf 2))
          (dfs Node(Leaf 7, Node(Leaf 3, Leaf 11)))
≡ list_eq 7::((dfs Node(Leaf 3, Leaf 11)) @ (dfs Leaf 2))
          ((dfs Leaf 7) @ (dfs Node(Leaf 3, Leaf 11))
≡ list_eq 7::((dfs Node(Leaf 3, Leaf 11)) @ (dfs Leaf 2))
          7::(dfs Node(Leaf 3, Leaf 11))
≡ list_eq ((dfs Node(Leaf 3, Leaf 11)) @ (dfs Leaf 2)) (dfs Node(Leaf 3, Leaf 11))
≡ list_eq ((dfs Leaf 3) @ (dfs Leaf 11)) @ (dfs Leaf 2)) (dfs Node(Leaf 3, Leaf 11))
≡ list_eq 3::((dfs Leaf 11) @ (dfs Leaf 2)) (dfs Node(Leaf 3, Leaf 11))
≡ list_eq 3::((dfs Leaf 11) @ (dfs Leaf 2)) ((dfs Leaf 3) @ (dfs Leaf 11))
≡ list_eq 3::((dfs Leaf 11) @ (dfs Leaf 2)) 3::(dfs Leaf 11)
≡ list_eq ((dfs Leaf 11) @ (dfs Leaf 2)) (dfs Leaf 11)
≡ list_eq 11::(dfs Leaf 2) (dfs Leaf 11)
≡ list_eq 11::(dfs Leaf 2) 11::[]
≡ list_eq (dfs Leaf 2) []
≡ list_eq 2::[] []

(we could write an eager OCaml function to compare trees this way…)

Also with lazy evaluation:

let rec nats n = n :: (nats (n+1))
let rec take n lst = match (n,lst) with
| (0,_) | (_,[]) -> []
| (_,(h::t)) -> h::(take (n-1) t)

Then (take 2 (nats 0)) is ok, even though (nats 0) doesn’t terminate:

(take 2 (nats 0))
(take 2 (0 :: (nats 1)))
0::(take 1 (nats 1))
0::(take 1 (1::(nats 2)))
0::1::(take 0 (nats 2))
0::1::[]

Other infinite objects:

let rec squares n = (n*n) :: (squares (n+1))
let rec sum = function
| [] -> 0
| x::xs -> x + sum xs

evaluating (sum (take 3 (squares 3))):

sum (take 3 (9::(squares 4)))
sum 9::(take 2 (squares 4))
9+(sum (take 2 (squares 4)))
9+(sum (take 2 (16::(squares 5))))
9+(sum 16::(take 1 (squares 5)))
9+16+(sum (take 1 (squares 5)))
25+(sum (take 1 (squares 5)))
25+(sum (take 1 25::(squares 6)))
25+(sum 25::(take 0 (squares 6)))...

Even with lazy evaluation, some expressions will fail to terminate, e.g.

List.exists ((>) 0) (nats 0)
List.exists ((>) 0) 0::(nats 1)
(0 > 0) || (List.exists ((>) 0) (nats 1))
false || (List.exists ((>) 0) (nats 1))
List.exists ((>) 0) (nats 1)
List.exists ((>) 0) 1::(nats 2)
(0 > 1) || (List.exists ((>) 0) (nats 2))
...

And

let rec whee x = whee (x+1)
List.exists (whee) [1]
≡ (whee 1) || (List.exists whee [])
≡ (whee 2) || (List.exists whee [])
≡ (whee 3) || (List.exists whee [])
...

cs2041.org

// reveal.js plugins