higher order functions · 2019. 7. 18. · a function that takes another function as input, or...

Post on 12-Sep-2020

7 Views

Category:

Documents

0 Downloads

Preview:

Click to see full reader

TRANSCRIPT

Higher Order FunctionsChapter 11 of Thompson

http://learnyouahaskell.com/higher-order-functions

A function that takes another function as input, or returns a function asoutput, is called a higher-order function.

Mastering higher order functions will make you a more productiveprogrammer.

Higher order functions

A first example

We met the parametric polymorphic identity function:

id :: a -> aid x = x

Function types are just like any other type, so…

id :: (Int -> Char) -> (Int -> Char)

Reminder about the associativity of ->

(Int -> Char) -> (Int -> Char)

Could be written as

(Int -> Char) -> Int -> Char

But not as Int -> Char -> (Int -> Char)or Int -> Char -> Int -> Char

You have already seen many functions in this course with functions asoutput – any function with more than one ‘input’!

f :: Int -> (String -> Char)

• Two inputs (an Int and a String), returning a Char ✓• One input (an Int), returning a function of type String -> Char ✓

The second point of view is called partial application.

Functions as output

multiply :: Int -> Int -> Intmultiply x y = x * y

multiply :: Int -> Int -> Intmultiply x y = x * y

multiply 2 3

26

3

multiply :: Int -> (Int -> Int)(multiply x) y = x * y

(multiply 2) 3

26

3

multiply :: Int -> Int -> Intmultiply x y = x * y

multiply 2 is a function Int -> Int that doubles its input!

2

Partial application

So just asmultiply :: Int -> Int -> Int

is convenient shorthand formultiply :: Int -> (Int -> Int)

We havemultiply 2 3

as convenient shorthand for(multiply 2) 3

In general:

f e1 e2 … ekt1 -> t2 -> ... tn -> t

are shorthands for:

((...((f e1) e2)...) ek)t1 -> (t2 -> (...(tn -> t)...))

i.e., function application is left-associative-> is right-associative

Functions as input

applyTwice:: (a -> a) -> a -> aapplyTwice f x = f (f x)

> applyTwice sqrt 162.0> applyTwice (+3) 1016

Functions as input

applyTwice:: (a -> a) -> a -> aapplyTwice f x = f (f x)

> applyTwice (++ " HAHA") "HEY""HEY HAHA HAHA”> applyTwice ("HAHA " ++) "HEY""HAHA HAHA HEY"> applyTwice (3:) [1][3,3,1]

Functions as input and output – Composition

Recall from the very first week that, given functionsf :: a -> b and g :: b -> c

We can define their composition, a functiong . f :: a -> c

So composition itself is a higher order function, with functions as both inputs and output.

Composition

(.) :: (b -> c) -> (a -> b) -> (a -> c)(.) g f x = g (f x)

We usually write (.) g f infix as g . f

> ((+1) . (*2)) 13> ((*2) . (+1)) 14

We have built-in functionseven :: Int -> Boolnot :: Bool -> Bool

Assume we want to definemyOdd :: Int -> BoolmyOdd x = not (even x)

more succinctly:

myOdd’ = not . even

Fold (also called “reduce”) functions

Often we wish to traverse a list once, element by element, building up an output as we go.

This is called a fold.

There are ‘right folds’ and ‘left folds’; we will try to understand right folds before we look at the left.

These are the most useful higher order function and are worth understanding – they will make you a more productive programmer!

Folds

Often we wish to traverse a list once, element by element, building up an output as we go.

• Add each number in a list to get the sum of all the elements;• Multiply each number in a list to get the product of all the elements;• Increment a number by 1 for each element to get the list’s length;• Turn a list of strings into an acronym;• Map a function over a list element by element;• etc…

mySum :: [Int] -> IntmySum list = case list of[] -> 0x:xs -> x + mySum xs

myLen :: [a] -> IntmyLen list = case list of[] -> 0x:xs -> 1 + myLen xs

myProd :: [Int] -> IntmyProd list = case list of[] -> 1x:xs -> x * myProd xs

acro :: [String] -> Stringacro list = case list of[] -> ""x:xs -> head x : acro xs

How are these definitions different? How are they the same?

mySum :: [Int] -> IntmySum list = case list of[] -> 0x:xs -> x + mySum xs

myLen :: [a] -> IntmyLen list = case list of[] -> 0x:xs -> 1 + myLen xs

myProd :: [Int] -> IntmyProd list = case list of[] -> 1x:xs -> x * myProd xs

acro :: [String] -> Stringacro list = case list of[] -> ""x:xs -> head x : acro xs

The names are different – but Haskell doesn’t really care about that

mySum :: [Int] -> IntmySum list = case list of[] -> 0x:xs -> x + mySum xs

myLen :: [a] -> IntmyLen list = case list of[] -> 0x:xs -> 1 + myLen xs

myProd :: [Int] -> IntmyProd list = case list of[] -> 1x:xs -> x * myProd xs

acro :: [String] -> Stringacro list = case list of[] -> ""x:xs -> head x : acro xs

The types are different – suggests folds are polymorphic

mySum :: [Int] -> IntmySum list = case list of[] -> 0x:xs -> x + mySum xs

myLen :: [a] -> IntmyLen list = case list of[] -> 0x:xs -> 1 + myLen xs

myProd :: [Int] -> IntmyProd list = case list of[] -> 1x:xs -> x * myProd xs

acro :: [String] -> Stringacro list = case list of[] -> ""x:xs -> head x : acro xs

The base cases are different

mySum :: [Int] -> IntmySum list = case list of[] -> 0x:xs -> x + mySum xs

myLen :: [a] -> IntmyLen list = case list of[] -> 0x:xs -> 1 + myLen xs

myProd :: [Int] -> IntmyProd list = case list of[] -> 1x:xs -> x * myProd xs

acro :: [String] -> Stringacro list = case list of[] -> ""x:xs -> head x : acro xs

The step cases are different – but only a little! – different recipes to combine the head with the recursive call on the tail

mySum :: [Int] -> IntmySum list = case list of[] -> 0x:xs -> x + mySum xs

myLen :: [a] -> IntmyLen list = case list of[] -> 0x:xs -> 1 + myLen xs

myProd :: [Int] -> IntmyProd list = case list of[] -> 1x:xs -> x * myProd xs

acro :: [String] -> Stringacro list = case list of[] -> ""x:xs -> head x : acro xs

Everything else is the same!

Fold Right

This suggests that we could define a polymorphic higher order function that takes as input• a base case;• a recipe for combining the head with a recursive call on the tailand then does all of the rest of the work for us!

No more time-consuming error-prone recursions to write• Except for all the ones that don’t follow the format of the previous slide

foldr

foldr in action

foldr :: (a -> b -> b) -> b -> [a] -> b

foldr :: (Int -> Int -> Int) -> Int -> [Int] -> Int

mySum = foldr (+) 0

myProd = foldr (*) 1

foldr in action

foldr :: (a -> b -> b) -> b -> [a] -> b

foldr :: (a -> Int -> Int) -> Int -> [a] -> Int

myLen = foldr (\x y -> y + 1) 0

Or, to avoid a warning:

myLen = foldr (\_ y -> y + 1) 0

foldr in action

foldr :: (a -> b -> b) -> b -> [a] -> b

foldr :: (String -> String -> String) -> String -> [String] -> String

acro = foldr (\x y -> head x : y) ""

How might you write a ‘safe’ version of acro that ignores empty strings instead of crashing on them?

foldr in action

foldr is just another function, and can be used as a helper anywhere:

myMaximum :: [Int] -> IntmyMaximum list = case list of[] -> error "No maximum of an empty list"x:xs -> foldr max x xs

Take some time to understand all the types here!

Defining foldr

myFoldr :: (a -> b -> b) -> b -> [a] -> bmyFoldr combine base list = case list of[] -> basex:xs -> combine x (myFoldr combine base xs)

Why is it fold right?

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

So the combining operation associates to the right.• i.e. start with the base case, combine it with the rightmost element of list,

then continue until we reach the leftmost element of the list.

foldr and the structure of lists

A list [1,2,3]is really 1 : (2 : (3 : []) )

foldr replaces [] with a base case and : with a combining function

e.g. 1 + (2 + (3 + 0) )

So folding right is very natural because lists themselves associate right

Left folds

mySuml :: [Int] -> IntmySuml = mySumAcc 0wheremySumAcc acc list = case list of[] -> accx:xs -> mySumAcc (acc + x) xs

In the above, mySumAcc has type Int -> [Int] -> Int

Left folds

mySuml [1,2,3]= mySumAcc 0 [1,2,3]= mySumAcc (0 + 1) [2,3]= mySumAcc ((0 + 1) + 2) [3]= mySumAcc (((0 + 1) + 2) + 3) []= (((0 + 1) + 2) + 3)

No difference for +, but not all combining operations are associative!

It folds the list up from the left side

Defining foldl

myFoldl :: (b -> a -> b) -> b -> [a] -> bmyFoldl combine acc list = case list of[] -> accx:xs -> myFoldl combine (combine acc x) xs

Compare to the final line of the right fold:

x:xs -> combine x (myFoldr combine base xs)

foldl, folding left

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

So the combining operation associates to the left.• i.e. start with the accumulator, combine it with the leftmost element of list,

then continue until we reach the empty list and return the accumulator.

foldr versus foldl

An example where they give different answers:

> foldr (-) 0 [1,2,3]2

> foldl (-) 0 [1,2,3]-6

Because ((0 – 1) – 2) – 3 ≠ 1 – (2 – (3 – 0))

foldr versus foldl

More generally, if we think of lists as built up from the empty list by using cons repeatedly, then lists are constructed from the right.

Therefore foldr tends to follow the way lists are constructed.

e.g. foldr (:) [] :: [a] -> [a] is the identity!

foldl goes in the reverse direction from the list’s construction• What happens if you use (:) with foldl?

top related