Real World Haskell: Lecture 6

38
Real World Haskell: Lecture 6 Bryan O’Sullivan 2009-11-11

Transcript of Real World Haskell: Lecture 6

Page 1: Real World Haskell: Lecture 6

Real World Haskell:Lecture 6

Bryan O’Sullivan

2009-11-11

Page 2: Real World Haskell: Lecture 6

Models of evaluation

Coming from a C++ or Python background, you’re surely used tothe the || and or operators in those languages.

I If the operator’s left argument evaluates to true, it “shortcircuits” the right (i.e. doesn’t evaluate it).

We see the same behaviour in Haskell.

Prelude> undefined*** Exception: Prelude.undefined

Prelude> True || undefinedTrue

Page 3: Real World Haskell: Lecture 6

Eager, or strict, evaluation

In most languages, the usual model of is of strict evaluation: afunction’s arguments are fully evaluated before the evaluation ofthe function’s body begins.

def inc(x):return x + 1

def bar(a):b = 5 if a % 2 else 8return inc(a + 3) * inc(inc(b))

For example, if we run bar(1) then:

I The local variable b gets the value 5.

I The two calls to inc are fully evaluated before we invoke * on5 and 7, respectively.

Page 4: Real World Haskell: Lecture 6

Are things different in Haskell?

Let’s think about our old friend the list constructor.

Prelude> 1:[][1]Prelude> 1:undefined[1*** Exception: Prelude.undefined

And a function that operates on a list:

Prelude> head (1:[])1

What do you think will happen now?

Prelude> head (1:undefined)1

Page 5: Real World Haskell: Lecture 6

Are things different in Haskell?

Let’s think about our old friend the list constructor.

Prelude> 1:[][1]Prelude> 1:undefined[1*** Exception: Prelude.undefined

And a function that operates on a list:

Prelude> head (1:[])1

What do you think will happen now?

Prelude> head (1:undefined)1

Page 6: Real World Haskell: Lecture 6

Was that a special case?

Well, uh, perhaps lists are special in Haskell. Riiight?

−− d e f i n e d i n Data . Maybei s J u s t ( Just ) = Truei s J u s t = False

Let’s see how the Maybe type fares.

Prelude Data.Maybe> Just undefinedJust *** Exception: Prelude.undefinedPrelude Data.Maybe> isJust (Just undefined)True

Page 7: Real World Haskell: Lecture 6

What else should we expect?

Here’s a slightly different function:

i s J u s t O n e ( Just a ) | a == 1 = Truei s J u s t O n e = False

How will it behave?

*Main> isJustOne (Just 2)False

Okay, we expected that. What if we follow the same pattern asbefore, and package up an undefined value?

*Main> isJustOne (Just undefined)*** Exception: Prelude.undefined

Page 8: Real World Haskell: Lecture 6

What else should we expect?

Here’s a slightly different function:

i s J u s t O n e ( Just a ) | a == 1 = Truei s J u s t O n e = False

How will it behave?

*Main> isJustOne (Just 2)False

Okay, we expected that. What if we follow the same pattern asbefore, and package up an undefined value?

*Main> isJustOne (Just undefined)*** Exception: Prelude.undefined

Page 9: Real World Haskell: Lecture 6

Haskell’s evaluation model

Haskell follows a semantic model called non-strict evaluation:expressions are not evaluated unless (and usually until) their valuesare used.

Perhaps you’ve heard of lazy evaluation: this is a specific kind ofnon-strict semantics.

Haskell compilers go further, using call by need as animplementation strategy.

Call by need: evaluate an expression when needed, then overwritethe location of the expression with the evaluated result(i.e. memoize it), in case it is needed again.

Page 10: Real World Haskell: Lecture 6

What does this mean in practice?

Consider the isJust function again.

i s J u s t ( Just ) = Truei s J u s t = False

It only evaluates its argument to the point of seeing whether it wasconstructed with a Just or Nothing constructor.

Notably, the function does not inspect the argument of the Justconstructor.

When we try isJust (Just undefined) in ghci, the valueundefined is never evaluated.

Page 11: Real World Haskell: Lecture 6

And our other example, revisited

Who can explain why this code crashes when presented withJust undefined?

i s J u s t O n e ( Just a ) | a == 1 = Truei s J u s t O n e = False

Page 12: Real World Haskell: Lecture 6

A classic example

Here is the infinite list of Fibonacci numbers in Haskell:

f i b s = 0 : 1 : zipWith (+) f i b s ( t a i l f i b s )

Even though this list is conceptually infinite, its components onlyget generated on demand.

*Main> head (drop 256 fibs)141693817714056513234709965875411919657707794958199867

Page 13: Real World Haskell: Lecture 6

Traversing lists

What do these functions have in common?

map f [ ] = [ ]map f ( x : x s ) = f x : map f x s

b u n z i p [ ] = ( [ ] , [ ] )b u n z i p ( ( a , b ) : x s ) = l e t ( as , bs ) = b u n z i p xs

i n ( a : as , b : bs )

*Main> map succ "button""cvuupo"*Main> bunzip [(1,’a’),(3,’b’),(5,’c’)]([1,3,5],"abc")

Page 14: Real World Haskell: Lecture 6

What does map do?

If you think about what map does to the structure of a list, itreplaces every (:) constructor with a new (:) constructor that hasa transformed version of its arguments.

map succ ( 1 : 2 : [ ] )== ( succ 1 : succ 2 : [ ] )

Page 15: Real World Haskell: Lecture 6

And bunzip?

Thinking structurally, bunzip performs the same kind of operationas map.

b u n z i p ( ( 1 , ’ a ’ ) : ( 2 , ’ b ’ ) : [ ] )== ( ( 1 : 2 : [ ] ) , ( ’ a ’ : ’ b ’ : [ ] ) )

This time, the pattern is much harder to see, but it’s still there:

I Every time we see a (:) constructor, we replace it with atransformed piece of data.

I In this case, the transformed data is the head pair pulled apartand grafted onto the heads of a pair of lists.

Page 16: Real World Haskell: Lecture 6

Abstraction! Abstraction! Abstraction!

If we have two functions that do essentially the same thing, don’twe have a design pattern?

In Haskell, we can usually do better than waffle about designpatterns: we can reify them into code! To wit:

f o l d r f z [ ] = zf o l d r f z ( x : x s ) = f x ( f o l d r f z xs )

Page 17: Real World Haskell: Lecture 6

The right fold (no, really)

The foldr function is called a right fold, because it associates tothe right. What do I mean by this?

f o l d r (+) 1 [ 2 , 3 , 4 ]== f o l d r (+) 1 (2 : 3 : 4 : [ ] )== f o l d r (+) 1 (2 : (3 : (4 : [ ] ) ) )== 2 + (3 + (4 + 1 ) )

Notice a few things:

I We replaced the empty list with the “empty” value.

I We replaced each non-empty constructor with an additionoperator.

I That’s our structural transformation in a nutshell!

Page 18: Real World Haskell: Lecture 6

map as a right fold

Because map follows the same pattern as foldr , we can actuallywrite map in terms of foldr!

bmap : : ( a −> b ) −> [ a ] −> [ b ]bmap f xs = f o l d r g [ ] x s

where g y ys = f y : ys

Since we can write a map as a fold, this implies that a fold issomehow more primitive than a map.

Page 19: Real World Haskell: Lecture 6

unzip as a right fold

And here’s our unzip-like function as a fold:

b u n z i p : : [ ( a , b ) ] −> ( [ a ] , [ b ] )b u n z i p xs = f o l d r g ( [ ] , [ ] ) x s

where g ( x , y ) ( xs , y s ) = ( x : xs , y : y s )

In fact, I’d suggest that bunzip-in-terms-of-foldr is actually easierto understand than the original definition.

Many other common list functions can be expressed as right folds,too!

Page 20: Real World Haskell: Lecture 6

Function composition (I)

Remember your high school algebra?

f ◦ g (x) ≡ f (g(x))

f ◦ g ≡ λx → f (g x)

Taking the above notation and writing it in Haskell, we get this:

f . g = \x −> f ( g x )

The backslash is Haskell ASCII art for λ, and introduces ananonymous function. Between the backslash and the ASCII arroware the function’s arguments, and following the arrow is its body.

Page 21: Real World Haskell: Lecture 6

Function composition (II)

f . g = \x −> f ( g x )

The result of f . g is a function that accepts one argument,applied f to it, then applies g to the result.

So the expression succ . succ adds two to a number, for instance.

Why care about this? Well, here’s our old definition ofmap-as-foldr .

bmap f xs = f o l d r g [ ] x swhere g y ys = f y : ys

And here’s a more succinct version.

bmap f xs = f o l d r ( ( : ) . f ) [ ] x s

Page 22: Real World Haskell: Lecture 6

The left fold

So we’ve seen folds that associate to the right:

1 + (2 + (3 + 4 ) )

What about folds that associate to the left?

( ( 1 + 2) + 3) + 4

Not surprisingly, the left fold does indeed exist, and is named foldl .

f o l d l f z [ ] = zf o l d l f z ( x : x s ) = f o l d l f ( f z x ) xs

Page 23: Real World Haskell: Lecture 6

The right fold in pictures

Page 24: Real World Haskell: Lecture 6

The left fold in pictures

Page 25: Real World Haskell: Lecture 6

Folds and laziness

Which of these definitions for adding up the elements of a list isbetter?

sum1 xs = f o l d r (+) 0 xssum2 xs = f o l d l (+) 0 xs

That’s a hard question to approach without a sense of what lazyevaluation will cause to happen.

Suppose an oracle generates the list [1..1000] for us at a rate ofone element per second.

Page 26: Real World Haskell: Lecture 6

Sum as right fold

In the first second, we see the partial expression

1 : ( . . . ) {− can ’ t s e e any th i ng more ye t −}

But we want to know the result as soon as possible, so wegenerate a partial result:

1 + ( . . . ) {− can ’ t make any more p r o g r e s s y e t −}

Page 27: Real World Haskell: Lecture 6

Second number two

In the second second, we now have the partial expression

1 : (2 : . . . ) {− can ’ t s e e any th i ng more ye t −}

We thus construct a little more of our eventual result:

1 + (2 + ( . . . ) ) {− s t i l l no f u r t h e r p r o g r e s s −}

Because we’re constructing a right-associative expression (that’swhat a right fold is for), we can’t create an intermediate result ofthe-sum-so-far at any point.

In other words, we’re creating a big expression containing 1000nested applications of (+), which we’ll only be able to fullyevaluate at the end of the list!

Page 28: Real World Haskell: Lecture 6

What happens in practice?

On casual inspection, it’s not clear that this right fold businessreally matters.

Prelude> foldr (+) 0 [0..100000]5000050000

But if we try to sum a longer list, we get a problem:

Prelude> foldr (+) 0 [0..1000000]*** Exception: stack overflow

The GHC runtime imposes a limit on the size of a deferredexpression to reduce the likelihood of us shooting ourselves in thefoot. Or at least to make the foot-shooting happen early enoughthat it won’t be a serious problem.

Page 29: Real World Haskell: Lecture 6

Left folds are better . . . uh, right?

Obviously, a left fold can’t tell us the sum before we reach the endof a list, but it has a promising property.

Given a list like this:

1 : 2 : 3 : . . .

Then our sum-via- foldl will produce a result like this:

( ( 1 + 2) + 3) + . . .

This is left associative, so we could potentially evaluate theleftmost portion on the fly: add 1 + 2 to give 3, then add 3 togive 6, and so on, keeping a single Int as the rolling sum-so-far.

Page 30: Real World Haskell: Lecture 6

Are we out of the woods?

So we know that this will fail:

Prelude> foldr (+) 0 [0..1000000]*** Exception: stack overflow

But what about this?

Prelude> foldl (+) 0 [0..1000000]*** Exception: stack overflow

Hey! Shouldn’t the left fold have saved our bacon?

Page 31: Real World Haskell: Lecture 6

Why did foldl not help?

Alas, consider the definition of foldl :

f o l d l : : ( a −> b −> a ) −> a −> [ b ] −> af o l d l f z [ ] = zf o l d l f z ( x : x s ) = f o l d l f ( f z x ) xs

Because foldl is polymorphic, there is no way it can inspect theresult of f z x.

And since the intermediate result of each f z x can’t be evaluated,a huge unevaluated expression piles up until we reach the end ofthe list, just as with foldr!

Page 32: Real World Haskell: Lecture 6

Tracing evaluation

How can we even get a sense of what pure Haskell code is actuallydoing?

import Debug . Trace

f o l d l T : : (Show a ) =>( a −> b −> a ) −> a −> [ b ] −> a

f o l d l T f z [ ] = zf o l d l T f z ( x : xs ) =

l e t i = f z xi n t r a c e ( ”now ” ++ show i ) f o l d l T f i x s

Page 33: Real World Haskell: Lecture 6

What does trace do?

The trace function is a magical function of sin: it prints its firstargument on stderr, then returns its second argument.

The expression trace (”now ”++ show i) foldlT printssomething, then returns foldT.

If you have the patience, run this in ghci:

Prelude> foldlT (+) 0 [0..1000000]now 0now 1now 3...blah blah blah...500000500000

Whoa! It eventually prints a result, where plain foldl failed!

Page 34: Real World Haskell: Lecture 6

What’s going on?

I In order to print an intermediate result, trace must evaluate itfirst.

I Haskell’s call-by-need evaluation ensures that an unevaluatedexpression will be overwritten with the evaluated result.

I Instead of a constantly-growing expression, we thus have asingle primitive value as the running sum at each iterationthrough the loop.

Page 35: Real World Haskell: Lecture 6

Is a debugging hack the answer to our problem?

Clearly, using trace seems like an incredibly lame solution to theproblem of evaluating intermediate results, although it’s very usefulfor debugging.

(Actually, it’s the only Haskell debugger I use.)

The real solution to our problem lies in a function named seq.

seq : : a −> t −> t

This function is a rather magical hack; all it does is evaluate itsfirst argument until it reaches a constructor, then return its secondargument.

Page 36: Real World Haskell: Lecture 6

Folding with seq

The Data.List module defines the following function for us:

f o l d l ’ : : ( a −> b −> a ) −> a −> [ b ] −> af o l d l ’ f z [ ] = zf o l d l ’ f z ( x : xs ) = l e t i = f z x

i n i ‘ seq ‘ f o l d l ’ f i x s

Let’s compare the two in practice:

Prelude> foldl (+) 0 [0..1000000]*** Exception: stack overflow

Prelude> import Data.ListPrelude> foldl’ (+) 0 [0..1000000]500000500000

Page 37: Real World Haskell: Lecture 6

Rules of thumb for folds

If you can generate your result lazily and incrementally, e.g. asmap does, use foldr .

If you are generating what is conceptually a single result (e.g. onenumber), use foldl ’ , because it will evaluate those trickyintermediate results strictly.

Never use plain old foldl without the little tick at the end.

Page 38: Real World Haskell: Lecture 6

Homework

For each of the following, choose the appropriate fold for the kindof result you are returning. You can find the type signature foreach function using ghci.

I Write concat using a fold.

I Write length using a fold.

I Write (++) using a fold.

I Write and using a fold.

I Write unwords using a fold.

I Write (\\) from Data.List using a fold.

For super duper bonus points:

I Write either foldr or foldl in terms of the other. (Hint 1:only one is actually possible. Hint 2: the answer is highlynon-obvious, and involves higher order functions.)