Higher-order functions
Relevant content from Chapter 7 of Programming in Haskell 2nd Edition
Basic concepts
Functions with multiple arguments are usually defined using currying
. That is, arguments are taken one at a time by exploiting the fact that functions can return functions as results.
add :: Int -> Int -> Int
add x y = x + y
-- means
add :: Int -> (Int -> Int)
add = \x -> (\y -> x + y)
It is also permissible to define a function that takes functions as arguments.
twice :: (a -> a) -> a -> a
twice f x = f (f x)
> twice (*2) 3
12
> twice reverse [1, 2, 3]
[1, 2, 3]
A function that takes a function as an argument is called a higher-order
function.
Processing lists
Standard prelude defines a number of higher-order
functions for processing lists.
map :: (a -> b) -> [a] -> [b]
map f xs = [f x | x <- xs]
> map (+1) [1, 3, 5, 7]
[2, 4, 6, 8]
> map even [1, 2, 3, 4]
[False, True, False, True]
> map reverse ["abc", "def", "ghi"]
["ghi", "def", "abc"]
Map can be applied to itself to process nested lists.
map (map (+ 1)) [[1, 2, 3], [4, 5]]
= { applying the outer map }
[map (+ 1) [1, 2, 3], map (+ 1) [4, 5]]
= { applying the inner maps }
[[2, 3, 4], [5, 6]]
The function map
can also be defined using recursion.
map :: (a -> b) -> [a] -> [b]
map f [] = []
map f (x:xs) = f x : map f xs
Another useful higher-order
library function is filter
, which selects all elements of a list that satisfy a predicate, where a predicate (or property) is a function that returns a logical value.
filter :: (a -> Bool) -> [a] -> [a]
filter p xs = [x | x <- xs, p x]
> filter even [1..10]
[2, 4, 6, 8, 10]
> filter (> 5) [1..10]
[6, 7, 8, 9, 10]
-- /= means not equals
> filter (/= ' ') "abc def ghi"
"abcdefghi"
The function filter
can be defined using recursion.
filter :: (a -> Bool) -> [a] -> [a]
filter p [] = []
filter p (x:xs) | p x = x : filter p xs
| otherwise = filter p xs
Other higher-order
functions for processing lists that are defined in the standard prelude.
-- Decides if all elements of a list satisfy a predicate
> all even [2, 4, 6, 8]
True
-- Decide if any element of a list satifies a predicate
> any odd [2, 4, 6, 8]
False
-- Select elements from a list while they satisfy a predicate
> takeWhile even [2, 4, 6, 7, 8]
[2, 4, 6]
-- Remove elements from a list while they satisfy a predicate
> dropWhile odd [1, 3, 5, 6, 7]
[6, 7]
The foldr
function
Many functions that take a list as their argument can be defined using recursion.
f [] = v
f (x:xs) = x # f xs
The function maps the empty list to a value v
and any non-empty list to an operator #
applied to the head of the list and the result of recursively processing the tail.
sum [] = 0
sum (x:xs) = x + sum xs
product [] = 1
product (x:xs) = x * product xs
or [] = False
or (x:xs) = x || or xs
and [] = True
and (x:xs) = x && and xs
The higher-order
library function foldr
(fold right) encapsulated this pattern. With the operator #
and the value v
as arguments.
sum :: Num a => [a] -> a
sum = foldr (+) 0
product :: Num a => [a] -> a
product = foldr (*) 1
or :: [Bool] -> Bool
or = foldr (||) False
and :: [Bool] -> Bool
and = foldr (&&) True
The new definition can also include explicit list arguments
sum xs = foldr (+) 0 xs
The foldr
function itself can be defined using recursion.
foldr :: (a -> b -> b) -> b -> [a] -> b
foldr f v [] = v
foldr f v (x:xs) = f x (foldr f v xs)
It is best to think of foldr f v
in a non-recursive manner, as simply replacing each cons operator in a list by the function f, and the empty list at the end by the value v.
sum [1, 2, 3]
= { applying sum }
foldr (+) 0 [1, 2, 3]
= { applying [,,] syntax }
foldr (+) 0 (1 : (2 : (3 : [])))
= { applying foldr }
1 + (2 + (3 + 0))
= { applying + }
6
foldr
can be used to define many more functions and reflects an operator that is assumed to associate to the right (fold right
).
length :: [a] -> Int
-- n represents the accumulator value v, which is initalized to 0 by foldr
-- and represents the count of elements seens so far in the list
length = foldr (\_ n -> 1 + n) 0
length [1, 2, 3]
= { applying length }
foldr (\_ n -> 1 + n) 0 [1, 2, 3]
= { applying [,,] syntax }
foldr (\_ n -> 1 + n) 0 (1 : (2 : (3 : [])))
= { applying foldr }
1 + (1 + (1 + 0))
= { applying + }
3
The foldl
function
It is possible to define recursive functions on lists using an operator that is assumed to associate to the left (fold left
).
sum :: Num a => [a] -> a
sum = sum' 0
where
sum' v [] = v
sum' v (x:xs) = sum' (v + x) xs
sum [1, 2, 3]
= { applying sum }
sum' 0 [1, 2, 3]
= { applying sum' }
sum' (0 + 1) [2, 3]
= { applying sum' }
sum' ((0 + 1) + 2) [3]
= { applying sum' }
sum' (((0 + 1) + 2) + 3) []
= { applying sum' }
(((0 + 1) + 2) + 3)
= { applying + }
6
The order of association does not affect the result in this case, because addition is associative. That is x + (y + z) = (x + y) + z
for any number x
, y
and z
.
Many functions on list can be defined using the following simple pattern of recursion.
f v [] = v
f v (x:xs) = f (v # x) xs
The function maps the empty list to the accumulator
value v
and any non-empty list to the result of recursively processing the tail, using a new accumulator
value obtained by applying an operator #
to the current value and the head of the list.
The higher-order
library function foldl
(fold left) encapsulated this pattern. With the operator #
and the accumulator
v
as arguments.
sum :: Num a => [a] -> a
sum = foldl (+) 0
product :: Num a => [a] -> a
product = foldl (*) 1
or :: [Bool] -> Bool
or = foldl (||) False
and :: [Bool] -> Bool
and = foldl (&&) True
The additional function defined above can also be rewritten to use foldl
instead of foldr
.
length :: [a] -> Int
length = foldl (\n _ -> n + 1) 0
reverse :: [a] -> [a]
reverse = foldl (\xs x -> x:xs) []
length [1, 2, 3] = ((0 + 1) + 1) + 1 = 3
reverse [1, 2, 3] = 3 : (2 : (1 : [])) = [3, 2, 1]
When a function can be defined using both foldr
and foldl
, the choice of which definition is preferable is made on grounds of efficiency and therefore requires consideration of the evaluation mechanism.
The foldl
function itself can be defined using recursion.
foldl :: (a -> b -> a) -> a -> [b] -> a
foldl f v [] = v
foldl f v (x:xs) = foldl f (f v x) xs
It is best to think of foldl f v
in a non-recursive manner. In terms of an operator #
that is assumed to associate to the left.
The composition operator
The higher-order
library operator .
returns the composition of two functions as a single function.
(.) :: (b -> c) -> (a -> b) -> (a -> c)
f . g = \x -> f (g x)
That is f . g
read as f
composed with g
, is the function that takes an argument x
, applies the function g
to it and applies the function f
to the result.
Composition can be used to simplify nested function applications by reducing parentheses and avoiding the need to explicitly refer to the initial argument.
-- Non composite version
odd n = not (even n)
twice f x = f (f x)
sumsqreven ns = sum (map (^ 2) (filter even ns))
-- Version making use of composition
odd = not . even
twice f = f . f
sumsqreven = sum . map (^ 2) . filter even
The last definition exploits the fact that composition is associative, meaning f . (g . h) = (f . g) . h
for any function f
, g
and h
.
Composition also has an identity.
id :: a -> a
id = \x -> x
That is id
is the function that simply returns its argument unchanged and has the property id . f = f
and f . id = f
for any function f
. Useful as a suitable starting point for a sequence of compositions.
compose :: [a -> a] -> (a -> a)
compose = foldr (.) id
Furthermore, definitions can often be written in a point-free
notation.
odd x = not (even x)
<=> { Syntactic sugar }
odd = \x -> not (even x)
<=> { Definition of (.) }
odd = \x -> (not . even) x
<=> ( Eta conversion / contraction )
odd = not . even
The above process uses eta conversion / contraction
. Which denotes the following equality.
(\x -> f x) == f
f . g = \x -> f (g x)
Additionally, the term point-free
does not refer to the .
character, but the fact that the definition does not mention the data points on which the functions act.
Every definition has a point-free
form that can be computed automatically, see PointFree.io for more information.
totalWordCount :: [String] -> Int
totalWordCount = \strs -> foldr (+) 0 ( map length (map words strs))
= {Definition of (.), eta conversion / contraction }
foldr (+) 0 . map (length . words)
= { applying "foldr f v . map g = foldr (f . g) v" }
foldr ((+) . length . words) 0