Lazy Evaluation in Haskell - GitLab · Lazy evaluation A function will only evaluate an argument if...

Preview:

Citation preview

Lazy Evaluation in HaskellChapter 17 of Thompson

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.

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

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!

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.

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

Lazy evaluation

Evaluations are over graphs.

+

(9-3)

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.

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)

Calculation rules and lazy expressions

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

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

f q1 q2 ... qk= ...

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

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

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

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

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.

Evaluation order

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

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

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

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

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)]

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)

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)]

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]]

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] ]

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]

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)]

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]

Time and space behaviourChapter 20

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.

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).

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)

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.

Space behaviour: where clauses

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

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)

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.

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

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.

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.

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.

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.

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.

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

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!

Recommended