15-150 Fall 2018 Lecture N Tuesday, 20 November I m too lazy to figure out the correct number Stephen Brookes
midterm2 Mean: 79.5 Std Dev: 18.4 Median: 84
today or, we could procrastinate lazy functional programming brought to you (on a plate) by
lazy programming Standard ML is call-by-value functions always evaluate their arguments Some other languages are lazy functions may defer argument evaluation until the argument value is needed You can still be lazy in a call-by-value language, but it takes effort!
being lazy Defer computation until its result is needed A function value, h : unit -> t (a thunk ) represents a delayed computation for a value of type t To compute the value, call h( ) to force the thunk
call-by-value repeat : a list -> a list fun repeat L = L @ repeat L lists are finite repeat evaluates to a function value repeat =>* (fn L => L @ repeat L) repeat [0] doesn t terminate repeat [0] =>* (fn L => L @ repeat L) [0] => [0] @ repeat [0] =>* [0] @ ([0] @ repeat [0]) =>*... forever there s no way to build an infinite list
lazy lists datatype a lazylist = a thunk Cons of a * (unit -> a lazylist) val Cons = fn - : a * (unit -> a lazylist) -> a lazylist A value of type t lazylist represents an infinite lazy list of values of type t The value has form Cons(v, h), where v : t is the head and h : unit -> t lazylist is (a thunk for) the tail the head is immediately available force h if you need later elements
lots of nothing zeros : unit -> int lazylist fun zeros ( ) = Cons(0, zeros) Zeros : int lazylist val Zeros = zeros( ) all ones ones : unit -> int lazylist fun ones ( ) = Cons(1, ones) Ones : int lazylist val Ones = ones( )
lazy naturals nats : int -> (unit -> int lazylist) fun nats n ( ) = Cons(n, nats (n+1)) Nats : int lazylist val Nats = Cons(0, nats 1) head tail { Nats represents 0,1,2,3,4,... nats 2 ( ) represents 2,3,4,5,6,...
mklazy mklazy : int list -> int lazylist fun mklazy [ ] = Zeros mklazy (x::l) = Cons(x, fn ( ) => mklazy L) Converts an int list to an int lazylist by padding with 0 s
representation A value of type t lazylist represents an infinite list of values of type t Zeros Ones nats 42 ( ) represents 0,0,0,0, represents 1,1,1,1, represents 42,43,44,45, mklazy [1,2,3] represents 1,2,3,0,0,0, An expression of type t lazylist represents if it evaluates to a value that represents
lazy append append : a list * (unit -> a lazylist) -> a lazylist fun append ([ ], h) = h( ) append (x::xs, h) = Cons(x, fn ( ) => append(xs, h)) What does append([0,1], nats 2) represent? append([0,1], nats 2) =>* Cons(0, h1) where h1( ) =>* Cons(1, h2) and h2( ) =>* Cons(2, h3) and so on append([0,1], nats 2) represents 0,1,2,3,4,
think thunks We said append([0,1], nats 2) =>* Cons(0, h1) where h1( ) =>* Cons(1, h2) It s not true that append([0,1], nats 2) =>* Cons(0, fn ( ) => Cons(1, h2)) That s not how ML works! No evaluation inside a function body.
lazy append If h( ) represents y0,y1,y2,y3, then append ([x0,,xk], h) represents x0,,xk,y0,y1,y2,y3,
repeat repeat : a list -> a lazylist REQUIRES xs [ ] ENSURES repeat(xs) represents xs repeated over and over fun repeat xs = append(xs, fn ( ) => repeat xs) repeat [0] =>*???
repeat [0] repeat [0] =>* Cons(0, h0) where h0( ) =>* Cons(0, h1) and h1( ) =>* Cons(0, h2) and so on repeat [0] represents 0,0,0,0,0,...
show show : int -> a lazylist -> a list fun show 0 _ = [ ] show n (Cons(x, h)) = x :: show (n-1) (h( )) show 5 (repeat [0]) evaluates to [0,0,0,0,0]
lazy equality Lazy list values A and B are equal iff they represent the same infinite sequence For all n, show n A = show n B. Lazy list expressions are equal if they both fail to terminate, or raise the same exception or they evaluate to equal values
lazy map map : ( a -> b) -> a lazylist -> b lazylist fun map f (Cons(x, h)) = Cons(f x, fn ( ) => map f (h( ))) map (fn x => x+1) Zeros??? If f is total and L represents x0,x1,x2, recursive call is deferred then map f L represents f(x0),f(x1),f(x2),
map is lazy List.map If f : t -> t is total, then for all L : t lazylist, and all n 0, show n (map f L) = List.map f (show n L) PROOF? Use induction on n map f (Cons(x, h)) = Cons(f x, fn ( ) => map f (h( ))) List.map f (x::xs) = (f x) :: List.map f xs show 0 _ = [ ] show n (Cons(x, h)) = x :: show (n-1) (h( ))
map is lazy List.map P(n): For all L : t lazylist, all total f, show n (map f L) = List.map f (show n L) THEOREM n 0. P(n) PROOF By induction on n Base: n=0. Both sides are [ ]. Inductive step: n > 0. We show that P(n-1) implies P(n). add justifications for the proof steps Assume IH: P(n-1). Let L be Cons(x, h) and v = f x. show n (map f L) = show n (map f (Cons(x, h))) = show n (Cons(f x, fn () => map f (h()))) = show n (Cons(v, fn () => map f (h()))) = v :: show (n-1) (map f (h())) = v :: List.map f (show (n-1) (h())) = List.map f (x ::(show (n-1) (h()))) = List.map f (show n (Cons(x, h))) = List.map f (show n L) So P(n) holds.
lazy map fusion If f and g are total and composable, then for all suitably typed lazy lists L, map (f o g) L = map f (map g L) same as map (f o g) = (map f) o (map g)
eager map eager_map : ( a -> b) -> a lazylist -> b lazylist fun eager_map f (Cons(x, h)) = let val L = eager_map f (h( )) in Cons(f x, fn ( ) => L) end eager_map (fn x => x+x) Ones???
lazy zip zip : a lazylist * b lazylist -> ( a * b) lazylist fun zip (Cons(x, h), Cons(y, k)) = Cons((x,y), fn ( ) => zip (h( ), k( ))) recursive call is deferred If A represents a0,a1,a2, and B represents b0,b1,b2, then zip(a,b) represents (a0,b0),(a1,b1),(a2,b2),
zip is lazy List.zip For all A : t1 lazylist and B : t2 lazylist and all n 0, show n (zip (A, B)) = List.zip (show n A, show n B) PROOF? Use induction on n zip (Cons(x, h), Cons(y, k)) = Cons((x, y), fn ( ) => zip (h( ), k( ))) List.zip (x::xs, y::ys) = (x, y) :: List.zip (xs, ys) show 0 _ = [ ] show n (Cons(x, h)) = x :: show (n-1) (h( ))
lazy filter filter : ( a -> bool) -> a lazylist -> a lazylist fun filter p (Cons(x, h)) = if p x then Cons(x, fn ( ) => filter p (h( ))) else filter p (h( )) if x doesn t satisfy p, filter the tail if x satisfies p, x is head of filtered list, defer filtering tail until needed
example fun nats n = Cons(n, fn ( ) => nats (n+1)) val Evens = filter (fn x => (x mod 2 = 0)) (nats 0) show 1 Evens = [0] show 2 Evens = [0,2] show 3 Evens = [0,2,4] show 4 Evens = [0,2,4,6] show 5 Evens = [0,2,4,6,8] Evens represents 0,2,4,6,8,10,. the lazy list of all non-negative even integers
warning What happens when we evaluate filter (fn x => x<0) (repeat [1])?
specification If p is total, L represents a0,a1,a2, and infinitely many ai satisfy p then filter p L represents the lazy list of all the ai that satisfy p For all n 0 there is an N such that show N (filter p L) = the first n elements of L that satisfy p
recursive laziness Some lazy lists can be generated recursively natural numbers factorials primes Fibonacci
fun facts facts : unit -> int lazylist fun facts( ) = Cons(1, fn ( ) => map (op * ) (zip (facts( ), nats 2 ( )))) facts( ) represents 1, 2, 6, 24, 120, For all n 1, show n (facts( )) = [1!,, n!]
lazy stuff To show the power of laziness we tackle some problems that build and manipulate infinite data prime number generation digitwise real arithmetic
Primes sift : int -> int lazylist -> int lazylist fun sift x = filter (fn y => y mod x <> 0) sieve : int lazylist -> int lazylist fun sieve (Cons(x, h)) = Cons(x, fn ( ) => sieve (sift x (h( )))) Primes : int lazylist val Primes = sieve(nats 2 ( )) Strike out the twos Strike out the threes...the Sieve of Eratosthenes
sift and sieve Suppose Cons(x, h) represents the integers 2 not divisible by the first n primes Then x is the n+1 th prime And sift x (h( )) represents the integers 2 not divisible by the first n+1 primes So sieve(nats 2 ( )) represents the primes
reals in decimal Can represent a real number in decimal d0.d1d2...dn... d0 = the integer part 0 dn 9 for n>0 The lazy list d 0, d 1, d2,..., d n,... represents the real number d 0 + (0.1)d 1 + (0.1) 2 d 2 + + (0.1) n d n +
eval10 eval10 : int list -> real fun eval10 [ ] = 0.0 eval10 (x::xs) = real x + 0.1 * eval10 xs eval10 [d 0, d 1, d2,..., d n ] = d 0 + (0.1)d 1 + (0.1) 2 d 2 + + (0.1) n d n
approx10 approx10 : int -> int lazylist -> real fun approx10 n L = eval10 (show (n+1) L) If L represents d 0, d 1, d2..., d n,... then approx10 n L represents d 0 + (0.1)d 1 + (0.1) 2 d 2 + + (0.1) n d n
representing reals L : int lazylist represents r : real if limn (approx10 n L) = r fun zeros( ) = Cons(0, zeros) fun nines( ) = Cons(9, nines) Cons(1, zeros) represents 1.0 Cons(0, nines) represents 1.0
validity A lazy list L is valid (decimal) iff all digits in the tail of L are between 0 and 9 Every real number has a valid representation not unique may also have invalid representations 0, 9, 0, 99, 0, 99,... 0, 9, 9, 9, 9, 9,... 1, 0, 0, 0, 0, 0,... invalid valid valid 3 lazy lists, each representing 1.0 : real
normality To do lazy real arithmetic, we need ways to build and maintain valid representations carry10 : int lazylist -> int lazylist norm10 : int lazylist -> int lazylist carry10 makes the next digit between 0 and 9 n = 10 * (n div 10) + (n mod 10) 0 n mod 10 9 norm10 makes all digits between 0 and 9, lazily without changing the real number
carry10 carry10 : int lazylist -> int lazylist REQUIRES L represents r, digits in tail of L 0 ENSURES carry10(l) represents r, has first tail digit between 0 and 9 fun carry10 (Cons(x, xs)) = let val (Cons(y, ys)) = xs( ) in Cons(x + y div 10, fn ( ) => Cons(y mod 10, ys)) end
example L 0, 10, 2, 12, 4, 14, 6,... carry10 L 1, 0, 2, 12, 4, 14, 6,... carry10 (Cons(1, zeros)) = Cons(1, zeros) carry10 (Cons(0, nines)) = Cons(0, nines)
norm10 norm10 : int lazylist -> int lazylist REQUIRES L represents r, digits in tail of L between 0 and 18 carry amount 1 ENSURES norm10(l) represents r, all digits in tail are between 0 and 9 fun norm10 (Cons(x, xs)) = let val Cons(y, ys) = xs( ) val N = Cons(x, fn ( ) => norm10 (Cons(y, ys))) in if y+1 < 10 then N else carry10 N end carry amount 1
add10 add10 : int lazylist * int lazylist -> int lazylist REQUIRES A represents a, B represents b, digits in tail of A & tail of B are decimal ENSURES add10(a,b) represents a+b, digits in tail are decimal fun add10(a, B) = norm10 (map (op +) (zip(a, B)))
example 0.123456790123456790... + 0.909090909090909090... 1.032547699214365881... val Y = repeat [0,1,2,3,4,5,6,7,9]; val Z = repeat [0,9]; show 9 (add10(y, Z)); val it = [1,0,3,2,5,4,7,6,9] : int list
lessons Reasoning about lazy data is not leisure Need to be careful about termination use specs involving show n for equational reasoning, use lazy notion of equality