This is an old revision of the document!
Higher-order functions
In our previous examples, we have seen functions such as (+)
or (*)
which are passed as parameter to other functions, e.g. foldr
. A function which expects another function as parameter is called a higher-order function.
One distinctive functional programming trait relies on defining and using higher-order functions. Haskell makes this programming style very natural.
Functions can be defined in several ways, e.g.
f x = x + 1
as anonymous functions:
f = \x -> x + 1
(which is read “lambda x …”) or using pattern matching:
f 0 = 1 f 1 = 2 f 2 = 3
Currying
Consider the following function definitions:
f1 x y = x + y f2 x = \y -> x + y f3 = \x -> \y -> x + y f4 = \x y -> x + y
Read syntactically:
- the first function expects two parameters and returns their additions
- the second function returns a function which adds y to x. For instance
(f2 4)
is a function which adds4
to an integer - the third function is similar to the previous one, but defined as an anonymous function
- the fourth function is an anonymous function definition with two parameters
There is no conceptual or functional difference between any of the above definitions. From a Haskell compiler's view all function definitions look exactly as the third one. For instance, (f1 1)
is a correct function call which will return a function adding 1
to its parameter. (f1 1)
is called a functional closure, because it creates (closes) a context in which the first parameter is equal to the value 1
.
At the same time, the function call ((f1 1) 2)
is correct, returns 3
, and is equivalent to (f1 1 2)
.
More generally, functions defined as f2
or f3
are called curried (or curry) functions (in honor of Haskell Curry, a mathematician which contributed to functional programming) — they receive parameters in turn. The other definitions are called uncurried.
We again stress that, in Haskell, there is no difference between curried and uncurried functions. This is not the case in other functional languages, e.g. Lisp, Scheme.
Other higher-order functions
Imagine we want to do a certain computation on each member of a list, individually. Let's say, double each element. If we think functionally, we want to take a list [a,b,c, …]
, a function f
, and build the list:
[f a, f b, f c, ...]
It turns out we can write such a function relying on fold:
pp_map f = foldr (\x y->(f x):y) []
There is also a shorter way to write pp_map
:
pp_map f = foldr ((:).f) []
In the expression (:).f
, .
(the dot) stands for functional composition. Consider the anonymous function:
\x->x+1
which takes a parameter x
and returns its natural successor. We can compose it as follows:
(\x -> 2*x).(\x->x+1)
to obtain the function \x → 2*(x+1)
. In general, the function call (f.g) x
is equivalent to f (g x)
.
To understand (:).f
let us write the cons (:)
as an anonymous function:
\h t -> h:t
Hence, the following expressions are equivalent:
((:).f) ((\h t -> h:t).f) ((\h->\t->h:t).f) (\t->(f h):t)
A natural mistake is to consider ((:).f)
as invalid, since it receives two parameters, and we cannot compose such a function with one receiving only one parameter. However, every function in Haskell receives one parameter and returns a value (which can be a function or a primitive value).
There are other examples of useful higher-order functions. We illustrate them using examples:
filter (>3) [1,2,3,4,5,2] zipWith (+) [1,2,3] [3,2,1]
Guess what each function does, by testing it on several lists.
Haskell implementation of foldl
In the previous lecture, we have defined the fold left procedure as:
foldl op acc [] = acc foldl op acc (h:t) = foldl op (op h acc) t
Instead, in Haskell, foldl
is implemented as follows:
foldl op acc [] = acc foldl op acc (h:t) = foldl op (op acc h) t
Note the call of the op
function.
An exercise in modular programming with higher-order functions
Let us consider the task of matrix multiplication in Haskell. Example:
1 2 3 1 0 0 1+3 2 2+3 4 2 5 4 5 6 x 0 1 1 = 4+6 5 5+6 = 10 5 11 7 8 9 1 0 1 7+9 8 8+9 16 8 17
Matrix representation in Haskell
The basic representation building-block in Haskell is the list. We can represent matrices in Haskell as a list where each element is a row (hence a list of elements). Example:
m = [[1,2,3],[4,5,6],[7,8,9]]
A good warmup exercise is to write a nice display function for matrices. We transform each element of a line into a string:
displayline l = map (\e->(show e)++" ") l
The code can be improved for legibility:
displayline = map ((++" ").show)
Next, we fold the list into a string with a newline character:
displayline l = foldr (++) "\n" (map ((++" ").show) l)
and simplify again, by expressing the pipeline function calls as functional composition:
displayline :: Show a => [a] -> [Char] displayline = (foldr (++) "\n") . (map ( (++ " ") . show ) )
Note that we need to explicitly state the type of display. Sometimes, in Haskell, an explicit type declaration is required. For now, we omit details.
Next, we apply the above process on all matrix lines:
display :: Show a => [[a]] -> [[Char]] display = let bind = foldr (++) "\n" in bind.(map (bind . (map ((++" ") . show ) ) ) )
Notice that we have separated the binding process, because we reuse it.
Finally, we make all matrices displayable:
instance (Show a) => Show [[a]] where show = let bind = foldr (++) "\n" in bind.(map (bind.(map ((++" ").show ) ) ) )
More details about this implementation (e.g. instances, classes) will be given in future lectures.
Matrix multiplication
Step 1: Transposition
Matrix multiplication operates on the lines of the first matrix and columns of the second. We transpose the second matrix, so that we now operate on lines on both matrices. The following code extracts the first line from a matrix m
:
map head m
A matrix m
without its first column is:
map tail m
Finally transposition is given by:
transpose ([]:_) = [] transpose m = (map head m) : transpose (map tail m)
Notice that the basis case corresponds to a list containing empty lists.
Step 2: Computing multiplication
To compute the i,j
th element of the multiplication matrix, we need to multiply per element the i
th line by the j
th column:
zipWith (*) li cj
and then add-up the values:
foldr (+) 0 (zipWith (*) li cj)
To obtain the i
th line of the multiplication matrix, we need to repeat the above process for each column of the second matrix, in other words, for each line of its transposition:
map (\col -> foldr (+) 0 (zipWith (*) li col) ) (transpose m2)
Finally, to obtain the multiplication matrix, we need to compute all its lines, hence:
mult m1 m2 = map (\line -> map (\col -> foldr (+) 0 (zipWith (*) line col) ) (transpose m2) ) m1
Matrices as images
A matrix can be used to represent a rasterized image (a collection of pixels). In this example, we consider that pixels can have values: ' ' (white), '.' (grey) and '*' (black).
Higher-order functions can be naturally used to represent image-transformations, for instance, flipping:
flipH = map reverse flipV = reverse
Rotations:
rotate90left = flipV.transpose rotate90right = flipH.transpose
The negative of an image:
invert = map (map (\x->if x=='*' then ' ' else '*') )
Scaling an image horizontally:
scalex = foldr (\h t->h:h:t) []
Scaling vertically:
scaley = map scalex
Balanced scale:
scale = scalex . scaley
Or just a random sequence of operations:
rand = foldr (.) id [rotate90left, invert, scale]