41
Lazy Evaluation in Haskell Chapter 17 of Thompson

Lazy Evaluation in Haskell - GitLab · Lazy evaluation A function will only evaluate an argument if its value is actually needed. For structured arguments only those parts neededare

  • Upload
    others

  • View
    11

  • Download
    0

Embed Size (px)

Citation preview

Page 1: Lazy Evaluation in Haskell - GitLab · Lazy evaluation A function will only evaluate an argument if its value is actually needed. For structured arguments only those parts neededare

Lazy Evaluation in HaskellChapter 17 of Thompson

Page 2: Lazy Evaluation in Haskell - GitLab · Lazy evaluation A function will only evaluate an argument if its value is actually needed. For structured arguments only those parts neededare

Lazy evaluation

A function will only evaluate an argument if its value is actually needed.For structured arguments only those parts needed are evaluated.For example, intermediate lists are not necessarily expensive.Also, lazy evaluation allows infinite strutures.

Page 3: Lazy Evaluation in Haskell - GitLab · Lazy evaluation A function will only evaluate an argument if its value is actually needed. For structured arguments only those parts neededare

Lazy evaluation

To evaluate f a1 a2 … ak simply substitute each ai for corresponding variables in the function.For example, given:

f x y = x + ythen:

f (9-3) (f 34 3)➝ (9-3) + (f 34 3)

and continuing:➝ 6 + (f 34 3)➝ 6 + (34 + 3)➝ 6 + 37➝ 43

Page 4: Lazy Evaluation in Haskell - GitLab · Lazy evaluation A function will only evaluate an argument if its value is actually needed. For structured arguments only those parts neededare

Lazy evaluation

But, consider:g x y = x + 12

then:g (9-3) (g 34 3)➝ (9-3) + 12➝ 6 + 12➝ 18

An argument that is not needed will not be evaluated!

Page 5: Lazy Evaluation in Haskell - GitLab · Lazy evaluation A function will only evaluate an argument if its value is actually needed. For structured arguments only those parts neededare

Lazy evaluation

More realistically, consider:switch :: Integer -> a -> a -> aswitch n x y

| n>0 = x| otherwise = y

Only one of the arguments is ever used.

Page 6: Lazy Evaluation in Haskell - GitLab · Lazy evaluation A function will only evaluate an argument if its value is actually needed. For structured arguments only those parts neededare

Lazy evaluation

Consider:h x y = x + x

So:h (9-3) (h 34 3)➝ (9-3) + (9-3)

But, duplicate arguments are evaluated at most once!➝ 6 + 6➝ 12

Page 7: Lazy Evaluation in Haskell - GitLab · Lazy evaluation A function will only evaluate an argument if its value is actually needed. For structured arguments only those parts neededare

Lazy evaluation

Evaluations are over graphs.

+

(9-3)

Page 8: Lazy Evaluation in Haskell - GitLab · Lazy evaluation A function will only evaluate an argument if its value is actually needed. For structured arguments only those parts neededare

Lazy evaluation

Consider:pm (x,y) = x+1

Now,pm (3+2,4-17)➝ (3+2)+1➝ 6

Only the first part of the argument is evaluated.

Page 9: Lazy Evaluation in Haskell - GitLab · Lazy evaluation A function will only evaluate an argument if its value is actually needed. For structured arguments only those parts neededare

Lazy evaluation

• arguments to functions are evaluated only when necessary• only the needed parts of arguments are evaluated• arguments are evaluated at most once (expressions are graphs)

Page 10: Lazy Evaluation in Haskell - GitLab · Lazy evaluation A function will only evaluate an argument if its value is actually needed. For structured arguments only those parts neededare

Calculation rules and lazy expressions

f p1 p2 ... pk| g1 = e1| g2 = e2...

| otherwise = erwherev1 a1,1 ... = r1...

f q1 q2 ... qk= ...

Page 11: Lazy Evaluation in Haskell - GitLab · Lazy evaluation A function will only evaluate an argument if its value is actually needed. For structured arguments only those parts neededare

Calculation rules and lazy expressions

In calculating f a1 a2 ... ak:1. Evaluate ai sufficiently to match pi2. If no match try second equation, and so on

Page 12: Lazy Evaluation in Haskell - GitLab · Lazy evaluation A function will only evaluate an argument if its value is actually needed. For structured arguments only those parts neededare

Calculation — pattern matching

Given:f :: [Int] -> [Int] -> Intf [] ys = 0f (x:xs) [] = 0f (x:xs) (y:ys) = x+y

thenf [1 .. 3] [1 .. 3]➝ f (1:[2..3])[1..3]➝ f (1:[2..3]) (1:[2 .. 3])➝ 1+1

Page 13: Lazy Evaluation in Haskell - GitLab · Lazy evaluation A function will only evaluate an argument if its value is actually needed. For structured arguments only those parts neededare

Calculation — guardsGiven:

f :: Int -> Int -> Int -> Intf m n p

| m>=n && m>=p = m| n>=m && n>=p = n| otherwise = p

f (2+3) (4-1) (3+9)?? (2+3)>=(4-1) && (2+3)>=(3+9)?? ➝ 5>=3 && 5>=(3+9)?? ➝ True && 5>=(3+9)?? ➝ 5>=(3+9)?? ➝ 5>=12?? ➝ False?? 3>=5 && 3>=12?? ➝ False && 3>=12?? ➝ False?? otherwise ➝ True

➝ 12

Page 14: Lazy Evaluation in Haskell - GitLab · Lazy evaluation A function will only evaluate an argument if its value is actually needed. For structured arguments only those parts neededare

Calculation — local definitions

Given:f :: Int -> Int -> Intf m n

| notNil xs = front xs| otherwise = n

wherexs = [m..n]

front (x:y:zs) = x+yfront [x] = xnotNil [] = FalsenotNil (_:_) = True

f 3 5?? notNil xs?? | where xs = [3..5]?? | ➝ 3:[4..5]?? ➝ notNil (3:4..5)?? ➝ True

➝ front xs| where xs = 3:[4..5]| ➝ 3:4:[5]

➝ 3+4➝ 7

Page 15: Lazy Evaluation in Haskell - GitLab · Lazy evaluation A function will only evaluate an argument if its value is actually needed. For structured arguments only those parts neededare

Operators and other expression forms

True && x = xFalse && x = FalseEvaluate second argument only in the case that the first is False.Other operators consume only the arguments they need:

[] == (x:xs) ➝ Falsewithout evaluating x or xs.if … then … else evaluates like a guard.case … evaluates like a pattern match.let … evaluates like a where clause.lambda evaluates like application of a named function.

Page 16: Lazy Evaluation in Haskell - GitLab · Lazy evaluation A function will only evaluate an argument if its value is actually needed. For structured arguments only those parts neededare

Evaluation order

Evaluation is from the outside in:f1 e1 (f2 e2 17)

—————————————————————————

Otherwise from left to right:f1 e1 + f2 e2_____ _____

Page 17: Lazy Evaluation in Haskell - GitLab · Lazy evaluation A function will only evaluate an argument if its value is actually needed. For structured arguments only those parts neededare

List comprehensions revisited

[ e | q1, …, qk ]where each qualifier qi has one of two forms:

• a generator p <- lExp for p a pattern and lExp an expression of list type• a test bExp which is a Boolean expression

Each qi can refer to variables used in patterns of q1, …, qi-1

Page 18: Lazy Evaluation in Haskell - GitLab · Lazy evaluation A function will only evaluate an argument if its value is actually needed. For structured arguments only those parts neededare

Examples

Given:pairs :: [a] -> [b] -> [(a,b)]pairs xs ys = [ (x,y) | x<-xs , y<-ys ]

thenpairs [1,2,3] [4,5]➝ [(1,4),(1,5),(2,4),(2,5),(3,4),(3,5)]

Page 19: Lazy Evaluation in Haskell - GitLab · Lazy evaluation A function will only evaluate an argument if its value is actually needed. For structured arguments only those parts neededare

Examples

Given:triangle :: Int -> [(Int,Int)]triangle n = [(x,y) | x <- [1..n], y <- [1..x]

thentriangle 3➝ [(1,1),(2,1),(2,2),(3,1),(3,2),(3,3)

Page 20: Lazy Evaluation in Haskell - GitLab · Lazy evaluation A function will only evaluate an argument if its value is actually needed. For structured arguments only those parts neededare

Examples

Given:pyTriple n

= [(x,y,z) | x <- [2..n], y <- [x+1 .. n],z <- [y+1..n], x*x + y*y == z*z]

thenpyTriple 100➝ [(3,4,5),(5,12,13),(6,8,10), ..., (65,72,97)]

Page 21: Lazy Evaluation in Haskell - GitLab · Lazy evaluation A function will only evaluate an argument if its value is actually needed. For structured arguments only those parts neededare

Calculating with list comprehensions

Write e{f/x} for expression e in which every free occurrence of x has been replaced by f. This is the substitution of f for x in e.

[ (x,y) | x<-xs ]{[2,3]/xs} = [ (x,y) | x<-[2,3]

(x + sum xs){(2,[3,4])/(x,xs)} = 2 + sum [3,4]]

Page 22: Lazy Evaluation in Haskell - GitLab · Lazy evaluation A function will only evaluate an argument if its value is actually needed. For structured arguments only those parts neededare

Calculating with list comprehensions

[e | v <-[a1..an], q2,…,qk]➝ [ e{a1/v} | q2{a1/v}, … , qk{a1/v}

++ … ++[ e{an/v} | q2{an/v}, … , qk{an/v} ]

For example:[x+y | x <- [1,2], isEven x, y <- [x..2*x] ]➝ [1+y | isEven 1, y <- [1..2*1] ] ++

[2+y | isEven 2, y <- [2..2*2] ]

Page 23: Lazy Evaluation in Haskell - GitLab · Lazy evaluation A function will only evaluate an argument if its value is actually needed. For structured arguments only those parts neededare

Calculating with list comprehensions

[e | True, q2,…,qk] ➝ [ e | q2,…,qk][e | False, q2,…,qk] ➝ []So, continuing the example:

➝ [1+y | False, y <- [1..2*1] ] ++[2+y | True, y <- [2..2*2] ]

➝ [2+y | y <- [2,3,4] ]➝ [2+2 | ] ++ [2+3 | ] ++ [2+4 | ]➝ [2+2] ++ [2+3] ++ [2+4]➝ [4,5,6]

Page 24: Lazy Evaluation in Haskell - GitLab · Lazy evaluation A function will only evaluate an argument if its value is actually needed. For structured arguments only those parts neededare

Calculating with list comprehensions

triangle 3➝ [ (x,y) | x <- [1..3], y <- [1..x] ]➝ [ (1,y) | y <- [1..1] ] ++

[ (2,y) | y <- [1..2] ] ++[ (3,y) | y <- [1..3] ]

➝ [(1,1) |] ++[(2,1) |] ++ [(2,2) |] ++ [(3,1) |] ++ [(3,2) |] ++ [(3,3) |]

➝ [(1,1),(2,1),(2,2),(3,1),(3,2),(3.3)]

Page 25: Lazy Evaluation in Haskell - GitLab · Lazy evaluation A function will only evaluate an argument if its value is actually needed. For structured arguments only those parts neededare

Calculating with list comprehensions

[ m*m | m <- [1..10], m*m<50 ]➝ [1*1 | 1*1<50] ++ [2*2 | 2*2<50] ++ …

[7*7 | 7*7<50] ++ [8*8 | 8*8<50] ++ …➝ [ 1 | True] ++ [ 4 | True] ++ …

[ 49 | True] ++ [64 | False] ++ …➝ [1,4,…,49]

Page 26: Lazy Evaluation in Haskell - GitLab · Lazy evaluation A function will only evaluate an argument if its value is actually needed. For structured arguments only those parts neededare

Time and space behaviourChapter 20

Page 27: Lazy Evaluation in Haskell - GitLab · Lazy evaluation A function will only evaluate an argument if its value is actually needed. For structured arguments only those parts neededare

Space behaviour: lazy evaluation

Rule of thumb for space is the size of the largest expression produced during evaluation.This is accurate for computing numbers or Booleans, but not for data structures.Lazy evaluation means partial results are printed, and discarded, once computed.

Page 28: Lazy Evaluation in Haskell - GitLab · Lazy evaluation A function will only evaluate an argument if its value is actually needed. For structured arguments only those parts neededare

Space behaviour: lazy evaluation

Consider:[m .. n]

| n >= m = m:[m+1 .. n]| otherwise = []

[1..n]?? n >= 1

➝ 1:[1+1 .. n]?? n >=2

➝ 1:[2 .. n]➝ 1:2:[2+1 .. n]➝ …➝ 1:2:3: … :n:[]

The underlined pieces are printed and discarded as soon as possible.To measure space complexity we look at the non-underlined part (the residual evaluation), which is of constant size.So, space complexity is O(n0).

Page 29: Lazy Evaluation in Haskell - GitLab · Lazy evaluation A function will only evaluate an argument if its value is actually needed. For structured arguments only those parts neededare

Space behaviour: where clauses

exam1 = [1..n] ++ [1..n]

Takes time O(n1)and space O(n0)but calculates [1..n] twice!

exam2 = list ++ listwherelist = [1..n] ++ [1..n]

After evaluating list, the whole of the list it is stored, giving space complexity O(n1)

Page 30: Lazy Evaluation in Haskell - GitLab · Lazy evaluation A function will only evaluate an argument if its value is actually needed. For structured arguments only those parts neededare

Space behaviour: where clauses

exam3= [1..n] ++ [last [=1..n]]

Space is O(n0)

exam4 = list ++ [last list]wherelist = [1..n]

Space is O(n1)This is a space leak, since we only need one element of list.

Page 31: Lazy Evaluation in Haskell - GitLab · Lazy evaluation A function will only evaluate an argument if its value is actually needed. For structured arguments only those parts neededare

Space behaviour: where clauses

Avoiding redundant computation is (usually) always sensible, but it comes at the cost of space.

Page 32: Lazy Evaluation in Haskell - GitLab · Lazy evaluation A function will only evaluate an argument if its value is actually needed. For structured arguments only those parts neededare

Saving space?

fac 0 = 1fac n = n * fac (n-1)Has O(n1) space complexity from:

n * ((n-1) * ... * (2 * (1 * 1)) ... )before it is evaluated.Alternative is to perform multiplication as we go:

newFac :: Integer -> IntegernewFac n = aFac n 1aFac :: Integer -> Integer -> IntegeraFac 0 p = paFac n p = aFac (n-1) (p*n)

Page 33: Lazy Evaluation in Haskell - GitLab · Lazy evaluation A function will only evaluate an argument if its value is actually needed. For structured arguments only those parts neededare

Saving space?newFac :: Integer -> IntegernewFac n = aFac n 1aFac :: Integer -> Integer -> IntegeraFac 0 p = paFac n p = aFac (n-1) (p*n)newFac n➝ aFac n 1➝ aFac (n-1) (1*n)

?? (n-1) == 0 ➝ False➝ aFac (n-2) (1*n*(n-1))➝ …➝ aFac 0 (1*n*(n-1)*(n-2)*…*2*1)➝ (1*n*(n-1)*(n-2)*…*2*1)

Still forms a large unevaluated expression, since its value is not needed until the end.

Page 34: Lazy Evaluation in Haskell - GitLab · Lazy evaluation A function will only evaluate an argument if its value is actually needed. For structured arguments only those parts neededare

Saving space?

Consider:aFac 0 p = paFac n p

| (p==p) = aFac (n-1) (p*n)

The guard test forces evaluation of the intermediate multiplications.This version has constant space behaviour.

aFac 4 1➝ aFac (4-1) (1*4)

?? (4-1) == 0 ➝ False?? (1*4) == (1*4) ➝ True

➝ aFac (3-1) (4*3)?? (3-1) == 0 ➝ False?? (4*3) == (4*3) ➝ True

➝ aFac (2-1) (12*2)➝ …➝ aFac 0 (24*1)➝ (24*1)➝ 24

Page 35: Lazy Evaluation in Haskell - GitLab · Lazy evaluation A function will only evaluate an argument if its value is actually needed. For structured arguments only those parts neededare

Strictness

A function is strict in an argument if the result is undefined whenever the argument is undefined.Examples:

(+) is strict in both arguments(&&) is strict in only its first argument:True && x = xFalse && x = False

A function that is not strict in an argument is said to be non-strict or lazy in that argument.

Page 36: Lazy Evaluation in Haskell - GitLab · Lazy evaluation A function will only evaluate an argument if its value is actually needed. For structured arguments only those parts neededare

Folding revisited: foldr

foldr :: (a -> b -> b) -> b -> [a] -> bfoldr f st [] = stfoldr f st (x:xs) = f x (foldr f st xs)

e.g., sorting a list by insertion sort:iSort = foldr ins []

foldr f st [] [a1, a2, ... , an-1 , an]➝ a1 `f` (a2 `f` ... `f` (an-1 `f` (an `f` st)) ...)Bracketing is to the right!If f is lazy in its second argument then output is possible from the head of the list.

Page 37: Lazy Evaluation in Haskell - GitLab · Lazy evaluation A function will only evaluate an argument if its value is actually needed. For structured arguments only those parts neededare

Folding revisited: foldr

Consider:map f = foldr ((:).f) []

map (+2) [1..n]➝ foldr ((:).(+2)) [] [1..n]➝ 1+2 : (foldr ((:).(+2)) [] [2..n])➝ 3 : (foldr ((:).(+2)) [] [2..n])➝ …So, this has space complexity O(n0), since the elements are output as they are calculated.

Page 38: Lazy Evaluation in Haskell - GitLab · Lazy evaluation A function will only evaluate an argument if its value is actually needed. For structured arguments only those parts neededare

Folding revisited: foldr

We can rewrite the earlier fac as:fac n = foldr (*) 1 [1..n]

So this has space complexity O(n1) since the multiplications stack up until the whole expression is formed, as they are bracketed to the right.

Page 39: Lazy Evaluation in Haskell - GitLab · Lazy evaluation A function will only evaluate an argument if its value is actually needed. For structured arguments only those parts neededare

Folding revisited: foldlfoldl :: (a -> b -> b) -> b -> [a] -> bfoldl f st [] = stfoldl f st (x:xs) = foldl f (f st x) xs

foldl f st [] [a1, a2, ... , an-1 , an]➝ (...((st `f` a1) `f` a2) `f` ... `f` an-1) `f` an

foldl (*) 1 [1..n]➝ foldl (*) (1*1) [2..n]➝ …➝ foldl (*) (...((1*1)*2)*...*n) []➝ (...((1*1)*2)*...*n)The problem is that foldl is not strict in its second argument.

Page 40: Lazy Evaluation in Haskell - GitLab · Lazy evaluation A function will only evaluate an argument if its value is actually needed. For structured arguments only those parts neededare

Folding revisited: foldl

Using seq::a->b->b:The effect of seq x y is to evaluate x and return y.So, given:

strict :: (a -> b) -> a -> bstrict f x = seq x (f x)

Now, strict f is a strict version of the function f. So, as strict version of foldl is:

Foldlʹ :: (a -> b -> a) -> a -> [b] -> aFoldlʹ f st [] = stFoldlʹ f st (x:xs) = strict (foldlʹ f) (f st x) xs

Page 41: Lazy Evaluation in Haskell - GitLab · Lazy evaluation A function will only evaluate an argument if its value is actually needed. For structured arguments only those parts neededare

Folding revisited: foldl

foldl’ :: (a -> b -> a) -> a -> [b] -> afoldl’ f st [] = stfoldl’ f st (x:xs) = strict (foldl’ f) (f st x) xs

foldlʹ (*) 1 [1..n]➝ foldlʹ (*) 1 [2..n]➝ foldlʹ (*) 2 [3..n]➝ foldlʹ (*) 6 [4..n]➝ …This is constant space!