Higher-Order Functions - Functional Programming
Higher-Order Functions - Functional Programming
Please attempt the entire worksheet in advance of the tutorial, and bring with you all
work, including (if a computer is involved) printouts of code and test results. Tutorials
cannot function properly unless you do the work in advance.
You may work with others, but you must understand the work; you cant phone a friend
during the exam.
Assessment is formative, meaning that marks from coursework do not contribute to the
final mark. But coursework is not optional. If you do not do the coursework you are
unlikely to pass the exams.
Attendance at tutorials is obligatory; please let your tutor know if you cannot attend.
Higher-order functions
Haskell functions are values, which may be processed in the same way as other data such as numbers,
tuples or lists. In this tutorial well use a number of higher-order functions, which take other functions
as arguments, to write succinct definitions for the sort of list-processing tasks that youve previously
coded explicitly using recursion or comprehensions.
The first part of the tutorial deals with three higher-order functions, map, filter, and fold. For
each of these you will be asked to write three functions. The second part deals with fold in some
more detail, and will ask you to write functions using both map and filter at the same time.
Map
Transforming every list element by a particular function is a common need when processing listsfor
example, we may want to
- add one to each element of a list of numbers,
- extract the first element of every pair in a list,
- convert every character in a string to uppercase, or
- add a grey background to every picture in a list of pictures.
The map function captures this pattern, allowing us to avoid the repetitious code that results from
writing a recursive function for each case.
Consider a function g defined in terms of an imaginary function f as follows:
g [] = []
g (x:xs) = f x : g xs
The function g can be written with recursion (as above), or with a comprehension, or with map: all
three definitions are equivalent.
1
x1 : x2 : ... : xn : []
f (x1 ) : f (x2 ) : ... : f (xn ) : []
g xs = [ f x | x <- xs ]
g xs = map f xs
Below right is the definition of map. Note the similarity to the recursive definition of g (below left).
As compared with g, map takes one additional argument: the function f that we want to apply to
each element.
Given map and a function that operates on a single element, we can easily write a function that
operates on a list. For instance, the function that extracts the first element of every pair can be
defined as follows (using fst :: (a,b) -> a):
Exercises
1. Using map and other suitable library functions, write definitions for the following:
(a) A function uppers :: String -> String that converts a string to uppercase.
(b) A function doubles :: [Int] -> [Int] that doubles every item in a list.
(c) A function penceToPounds :: [Int] -> [Float] that turns prices given in pence into
the same price in pounds.
(d) Write a list-comprehension version of uppers and use it to check your answer to (a).
Filter
Removing elements from a list is another common need. For example, we might want to remove
non-alphabetic characters from a string, or negative integers from a list. This pattern is captured
by the filter function.
Consider a function g defined in terms of an imaginary predicate p as follows (a predicate is just a
function into a Bool value):
g [] = []
g (x:xs) | p x = x : g xs
| otherwise = g xs
The function g can be written with recursion (as above), or with a comprehension, or with filter:
all three definitions are equivalent.
2
g xs = [ x | x <- xs, p x ]
g xs = filter p xs
For instance, we can write a function evens :: [Int] -> [Int], which removes all odd numbers
from a list using filter and the standard function even :: Int -> Int:
Below right is the definition of filter. Note the similarity to the way g is defined (below left).
As compared with g, filter takes one additional argument: the predicate that we use to test each
element.
Exercises
2. Using filter and other standard library functions, write definitions for the following:
(a) A function alphas :: String -> String that removes all non-alphabetic characters
from a string.
(b) Define a function rmChar :: Char -> String -> String that removes all occurrences
of a character from a string.
(c) A function above :: Int -> [Int] -> [Int] that removes all numbers less than or
equal to a given number.
(d) A function unequals :: [(Int,Int)] -> [(Int,Int)] that removes all pairs (x,y)
where x == y.
(e) Write a list-comprehension version of rmChar and use QuickCheck to test it against the
version using filter.
3
Fold
The map and filter functions act on elements individually; they never combine one element with
another.
Sometimes we want to combine elements using some operation. For example, the sum function can
be written like this:
sum [] = 0
sum (x:xs) = x + sum xs
Here were essentially just combining the elements of the list using the + operation. Another example
is reverse, which reverses a list:
reverse [] = []
reverse (x:xs) = reverse xs ++ [x]
This function is just combining the elements of the list, one by one, by appending them onto the
end of the reversed list. This time the combining function is a little harder to see. It might be
easier if we wrote it this way:
reverse [] = []
reverse (x:xs) = x snoc reverse xs
snoc x xs = xs ++ [x]
Now you can see that snoc plays the same role as + played in the example of sum.
These examples (and many more) follow a pattern: we break down a list into its head (x) and tail
(xs), recurse on xs, and then apply some function to x and the modified xs. The only things we
need to specify are the function (such as (+) or snoc) and the initial value (such as 0 in the case of
sum and [] in the case of reverse.
This pattern is called a fold and is implemented in Haskell via the function foldr.
The function g can be written with recursion (as above) or by using a fold: both definitions are
equivalent.
g xs = foldr f u xs
One way to visualize the action of foldr is shown in Figure 2. Given a function f :: a -> b -> b,
an initial value u :: b (sometimes called the unit), and a list [x1 , x2 , ..., xn ] of type [a], the foldr
function returns the value that results from replacing every : (cons) in list with f and replacing
the terminating [] (nil) with u.
x1 : (x2 : : (xn : [] ) )
x1 f (x2 f f (xn f u ) )
For example, we can define sum :: [Int] -> Int as follows, using (+) as the function and 0 as
the initial value (unit):
4
sum :: [Int] -> Int
sum ns = foldr (+) 0 ns
(Note: to treat an infix operator like + as a function name, we need to wrap it in parentheses.)
10 : 20 : 30 : []
10 + 20 + 30 + 0
Exercises
4. We will practice the use of foldr by writing several functions first with recursion, and then
using foldr. You can use other standard library functions as well. For each pair of functions
that you write, test them against each other using QuickCheck.
(a) Look at the recursive function productRec :: [Int] -> Int that computes the prod-
uct of the numbers in a list, and write an equivalent function productFold using foldr.
(b) Write a recursive function andRec :: [Bool] -> Bool that checks whether every item
in a list is True. Then, write the same function using foldr, this time called andFold.
(c) Write a recursive function concatRec :: [[a]] -> [a] that concatenates a list of lists
into a single list. Then, write a similar function concatFold using foldr.
(d) Write a recursive function rmCharsRec :: String -> String -> String that removes
all characters in the first string from the second string, using your function rmChar from
exercise (2b). Then write the same function rmCharsFold using foldr.
*Main> rmCharsRec [a..l] "football"
"oot"
Then, write a second version rmCharsFold using rmChar and foldr. Check your func-
tions with QuickCheck.
Matrix manipulation
Next, we will look at matrix addition and multiplication. As matrices we will use lists of lists of
Ints; for example:
1 4 9 [[1,4,9],
is represented as
2 5 7 [2,5,7]]
The declaration below, which you can find in your tutorial3.hs, makes the type Matrix a shorthand
for the type [[Int]].
Our first task is to write a test to show whether a list of lists of Ints is a matrix. This test should
verify two things: 1) that the lists of Ints are all of equal length, and 2) that there is at least one
row and one column in the list of lists.
Exercises
5. (a) Write a function uniform :: [Int] -> Bool that tests whether the integers in a list
are all equal. You can use the library function all, which tests whether all the elements
of a list satisfy a predicate; check the type to see how it is used. If you want, you can
try to define all in terms of foldr and map.
5
(b) Using your function uniform write a function valid :: Matrix -> Bool that tests
whether a list of lists of Ints is a matrix (it should test the properties 1) and 2) specified
above).
A useful higher-order function is zipWith. It is a lot like the function zip that you have seen,
which takes two lists and combines the elements in a list of pairs. The difference is that instead of
combining elements as a pair, you can give zipWith a specific function to combine each two elements.
The definition is as follows (Figure 4 gives an illustration):
zipWith f [] _ = []
zipWith f _ [] = []
zipWith f (x:xs) (y:ys) = f x y : zipWith f xs ys
f (x1 )(y1 ) : (f (x2 )(y2 ) : ... : (f (xn )(yn ) : [] ) . . .)
Another useful function for working with pairs is uncurry, which turns a function that takes two
arguments into a function that operates on a pair.
Exercises
6. (a) Look up the definition of uncurry. What is returned by the following expression?
Main> uncurry (+) (10,8)
(b) Show how to define zipWith using zip and a list comprehension.
(c) Show how to define zipWith using zip and the higher-order functions map and uncurry,
instead of the list comprehension.
Adding two matrices of equal size is done by pairwise adding the elements that are in the same
position, i.e. in the same column and row, to form the new element at that position. For example:
1 2 3 10 20 30 11 22 33
+ =
4 5 6 40 50 60 44 55 66
For matrix multiplication we need what is called the dot product or inner product of two vectors:
(a1 , a2 , . . . , an ) (b1 , b2 , . . . , bn ) = a1 b1 + a2 b2 + . . . + an bn
Matrix multiplication is then defined as follows: two matrices with dimensions (n, m) and (m, p) are
multiplied to form a matrix of dimension (n, p) in which the element in row i, column j is the dot
6
product of row i in the first matrix and column j in the second. For example:
1 10 1 2 31 42
=
100 10 3 4 130 240
7
Optional Material
For a real challenge, you can try to compute the inverse of a matrix. There are a few steps
involved in this process:
(a) The entries of a matrix should be changed to Doubles or (even better) Rationals to
allow proper division.
(b) You will need a function to find the determinant of a matrix. This will tell you if it has
an inverse.
(c) You will need a function to do the actual inversion.
There are several different algorithms available to compute the determinant and the inverse
of a matrix. Good places to start looking are:
http://mathworld.wolfram.com/MatrixInverse.html
http://en.wikipedia.org/wiki/Invertible_matrix
Finally, implement an appropriate quickCheck test for your function.