Laziness Advanced functional programming - Lecture 3a Trevor L. - - PowerPoint PPT Presentation

laziness
SMART_READER_LITE
LIVE PREVIEW

Laziness Advanced functional programming - Lecture 3a Trevor L. - - PowerPoint PPT Presentation

Laziness Advanced functional programming - Lecture 3a Trevor L. McDonell (& Wouter Swierstra) 1 Laziness 2 square :: Integer -> Integer square x = x * x square (1 + 2) = -- magic happens in the computer 9 A simple expression How do


slide-1
SLIDE 1

Laziness

Advanced functional programming - Lecture 3a

Trevor L. McDonell (& Wouter Swierstra)

1

slide-2
SLIDE 2

Laziness

2

slide-3
SLIDE 3

A simple expression

square :: Integer -> Integer square x = x * x square (1 + 2) = -- magic happens in the computer 9 How do we reach that final value?

3

slide-4
SLIDE 4

Strict or eager or call-by-value evaluation

In most programming languages:

  • 1. Evaluate the arguments completely
  • 2. Evaluate the function call

square (1 + 2) = -- evaluate arguments square 3 = -- go into the function body 3 * 3 = 9

4

slide-5
SLIDE 5

Non-strict or call-by-name evaluation

Arguments are replaced as-is in the function body square (1 + 2) = -- go into the function body (1 + 2) * (1 + 2) = -- we need the value of (1 + 2) to continue 3 * (1 + 2) = 3 * 3 = 9

5

slide-6
SLIDE 6

Does call-by-name make any sense?

In the case of square, non-strict evaluation is worse Is this always the case? const x y = y

  • - forget about x
  • - Call-by-value
  • - Call-by-name

const (1 + 2) 5 const (1 + 2) 5 = = const 3 5 5 = 5

6

slide-7
SLIDE 7

Does call-by-name make any sense?

In the case of square, non-strict evaluation is worse Is this always the case? const x y = y

  • - forget about x
  • - Call-by-value
  • - Call-by-name

const (1 + 2) 5 const (1 + 2) 5 = = const 3 5 5 = 5

6

slide-8
SLIDE 8

Sharing expressions

square (1 + 2) = (1 + 2) * (1 + 2) Why redo the work for (1 + 2)? We can share the evaluated result square (1 + 2) = Δ * Δ ↑___↑___ (1 + 2) = 3 = 9

7

slide-9
SLIDE 9

Sharing expressions

square (1 + 2) = (1 + 2) * (1 + 2) Why redo the work for (1 + 2)? We can share the evaluated result square (1 + 2) = Δ * Δ ↑___↑___ (1 + 2) = 3 = 9

7

slide-10
SLIDE 10

Lazy evaluation

Haskell uses a lazy evaluation strategy

  • Expressions are not evaluated until needed
  • Duplicate expressions are shared

Lazy evaluation never requires more steps than call-by-value Each of those not-evaluated expressions is called a thunk

8

slide-11
SLIDE 11

Does it matter?

Is it possible to get different outcomes using different evaluation strategies? Yes and no

9

slide-12
SLIDE 12

Does it matter?

Is it possible to get different outcomes using different evaluation strategies? Yes and no

9

slide-13
SLIDE 13

Does it matter? - Correctness and efficiency

The Church-Rosser Theorem states that for terminating programs the result of the computation does not depend on the evaluation strategy But…

  • 1. Performance might be different
  • As square and const show
  • 2. This applies only if the program terminates
  • What about infinite loops?
  • What about exceptions?

10

slide-14
SLIDE 14

Termination

loop x = loop x

  • This is a well-typed program
  • But loop 3 never terminates
  • - Eager
  • - Lazy

const (loop 3) 5 const (loop 3) 5 = = const (loop 3) 5 5 = ... Lazy evaluation terminates more often than eager

11

slide-15
SLIDE 15

Build your own control structures

if_ :: Bool -> a -> a -> a if_ True t _ = t if_ False _ e = e

  • In eager languages, if_ evaluates both branches
  • In lazy languages, only the one being selected

For that reason,

  • In eager languages, if has to be built-in
  • In lazy languages, you can build your own control structures

12

slide-16
SLIDE 16

Short-circuiting

(&&) :: Bool -> Bool -> Bool False && _ = False True && x = x

  • In eager languages, x && y evaluates both conditions
  • But if the first one fails, why bother?
  • C/Java/C# include a built-in short-circuit conjunction
  • In Haskell, x && y only evaluates the second argument if the first one is

True

  • False && (loop True) terminates

13

slide-17
SLIDE 17

“Until needed”

How does Haskell know how much to evaluate?

  • By default, everything is kept in a thunk
  • When we have a case distinction, we evaluate enough to distinguish

which branch to follow take 0 _ = [] take _ [] = [] take n (x:xs) = x : take (n-1) xs

  • If the number is 0 we do not need the list at all
  • Otherwise, we need to distinguish [] from x:xs

14

slide-18
SLIDE 18

Weak Head Normal Form

An expression is in weak head normal form (WHNF) if it is:

  • A constructor with (possibly non-evaluated) data inside
  • True or Just (1 + 2)
  • An anonymous function
  • The body might be in any form
  • \x -> x + 1 or \x -> if_ True x x
  • A built-in function applied to too few arguments

Every time we need to distinguish the branch to follow the expression is evaluated until its WHNF

15

slide-19
SLIDE 19

Case study: foldl'

From long, long time ago… foldl _ v [] = v foldl f v (x:xs) = foldl f (f v x) xs foldl (+) 0 [1,2,3] = foldl (+) (0 + 1) [2,3] = foldl (+) ((0 + 1) + 2) [3] = foldl (+) (((0 + 1) + 2) + 3) [] = ((0 + 1) + 2) + 3

16

slide-20
SLIDE 20

Case study: foldl'

foldl (+) 0 [1,2,3] = ((0 + 1) + 2) + 3

  • Each of the additions is kept in a thunk
  • Some memory need to be reserved
  • They have to be GC’ed after use

17

slide-21
SLIDE 21

Case study: foldl'

18

slide-22
SLIDE 22

Case study: foldl'

Just performing the addition is faster!

  • Computers are fast at arithmetic
  • We want to force additions before going on

foldl (+) 0 [1,2,3] = foldl (+) (0 + 1) [2,3] = foldl (+) 1 [2,3] = foldl (+) (1 + 2) [3] = foldl (+) 3 [3] = foldl (+) (3 + 3) [] = foldl (+) 6 [] = 6

19

slide-23
SLIDE 23

Forcing evaluation

Haskell has a primitive operation to force seq :: a -> b -> b A call of the form seq x y

  • First evaluates x up to WHNF
  • Then it proceeds normally to compute y

Usually, y depends on x somehow

20

slide-24
SLIDE 24

Case study: foldl'

We can write a new version of foldl which forces the accumulated value before recursion is unfolded foldl' _ v [] = v foldl' f v (x:xs) = let z = f v x in z `seq` foldl' f z xs This version solves the problem with addition

21

slide-25
SLIDE 25

Case study: foldl'

22

slide-26
SLIDE 26

Strict application

Most of the times we use seq to force an argument to a function, that is, strict application ($!) :: (a -> b) -> a -> b f $! x = x `seq` f x Because of sharing, x is evaluated only once foldl' _ v [] = v foldl' f v (x:xs) = ((foldl' f) $! (f v x)) xs

23

slide-27
SLIDE 27

Profiling

24

slide-28
SLIDE 28

Something about (in)efficiency

We have seen that Haskell programs:

  • can be very short
  • and sometimes very inefficient

Question: How to find out where time is spent? Answer: Use profiling

25

slide-29
SLIDE 29

Something about (in)efficiency

We have seen that Haskell programs:

  • can be very short
  • and sometimes very inefficient

Question: How to find out where time is spent? Answer: Use profiling

25

slide-30
SLIDE 30

Laziness is a double-edged sword

  • With laziness, we are sure that things are evaluated only as much as

needed to get the result.

  • But, being lazy means holding lots of thunks in memory:
  • Memory consumption can grow quickly.
  • Performance is not uniformly distributed.

Question: How to find out where memory is spent? How to find out where to sprinkle seqs? Answer: Use profiling

26

slide-31
SLIDE 31

Laziness is a double-edged sword

  • With laziness, we are sure that things are evaluated only as much as

needed to get the result.

  • But, being lazy means holding lots of thunks in memory:
  • Memory consumption can grow quickly.
  • Performance is not uniformly distributed.

Question: How to find out where memory is spent? How to find out where to sprinkle seqs? Answer: Use profiling

26

slide-32
SLIDE 32

Example: segs

segs xs computes all the consecutive sublists of xs. segs [] = [[]] segs (x:xs) = segs xs ++ map (x:) (inits xs) > segs [2,3,4] [[],[4],[3],[3,4],[2],[2, 3],[2,3,4]] This implementation is extremely inefficient.

27

slide-33
SLIDE 33

Example: segsinits

We can compute inits and segs at the same time. segsinits [] = ([[]], [[]]) segsinits (x:xs) = let (segsxs, initsxs) = segsinits xs newinits = map (x:) initsxs in (segsxs ++ newinits, [] : newinits) segs = fst . segsinits

28

slide-34
SLIDE 34

Heap profile for segsinits

sds-prof +RTS -p -hc 440,761,105 bytes x seconds Wed Mar 8 16:57 2006

seconds 0.0 2.0 4.0 6.0 8.0 10.0 12.0 14.0 16.0 18.0 20.0 bytes 0M 2M 4M 6M 8M 10M 12M 14M 16M 18M 20M (157)/segsinits/segs/mainM...

29

slide-35
SLIDE 35

Example: pointfree

pointfree = let p = not . null next = filter p . map tail . filter p in concat . takeWhile p . iterate next . inits

30

slide-36
SLIDE 36

Heap profile for pointfree

pointfree-prof +RTS -p -hc 672,567 bytes x seconds Wed Mar 8 16:57 2006

seconds 0.0 0.2 0.4 0.6 0.8 1.0 1.2 1.4 1.6 bytes 0k 50k 100k 150k 200k 250k 300k 350k 400k 450k (155)/mainMain.CAF

31

slide-37
SLIDE 37

Example: listcomp

segs are just the tails of the inits! listcomp xs = [] : [ t | i <- inits xs , t <- tails i , not (null t) ] main = print (length (concat (listcomp [1 :: Int .. 300])))

32

slide-38
SLIDE 38

Heap profile for listcomp

listcomp-prof +RTS -p -hc 17,202 bytes x seconds Wed Mar 8 17:23 2006

seconds 0.0 0.2 0.4 0.6 0.8 1.0 1.2 1.4 bytes 0k 2k 4k 6k 8k 10k 12k (154)main

33

slide-39
SLIDE 39

How to produce these?

prompt> ghc -prof -auto-all -o listcomp-prof

  • O2 Segments.hs

prompt> ./listcomp-prof +RTS -hc -p 4545100 prompt> hp2ps listcomp-prof.hp

34