Not logged in. Login

List Processing Continued: Folds, Comprehensions and Beyond

Folds

Very often, we want to combine all the elements of a list together in some manner. Some examples:

  • the sum of a list of integers
add_all :: [Integer] -> Integer
add_all []     = 0
add_all (x:xs) = x + add_all xs
  • the product of a list of numeric values
mul_all :: [Integer] -> Integer
mul_all []     = 1
mul_all (x:xs) = x * (mul_all xs)
  • the concatenation of a list of string values
join_all :: [[Char]] -> [Char]
join_all []     = []
join_all (x:xs) = x ++ (join_all xs)
  • the conjunction of a list of boolean values
all_true :: [Bool] -> Bool
all_true []     = True
all_true (x:xs) =  x && (all_true xs)

Each of these functions exhibits a common computational pattern, known in Haskell as foldr: fold from the right.

add_all x = foldr (+) 0 x
mul_all x = foldr (*) 1 x
join_all x = foldr (++) [] x
all_true x = foldr (&&) True x

In general, folding is the repeated application of a binary function f to a list of elements starting with an initial accumulator value acc.

foldr :: (a -> b -> b) -> b -> [a] -> b
foldr f acc []     = acc
foldr f acc (x:xs) = f x (foldr f acc xs)

In general, the type of list elements a can differ from the type of the accumulated result b. (In which of our examples above are the types actually different?)

Folding from the right means that the list elements are combined into the result in right-to-left order.

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

Folding from left

Folding from the right may not always give what we expect:

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

This is because of the right-to-left processing:

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

Instead we can use foldl for right-to-left processing.

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

This reflects the left-to-right processing that we might expect.

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

Simplified folding

In many cases, when the type of the result being computed is the same as the list element type, we can simplify the folding by taking the initial element being accumulated from the list itself.

The foldr1 function uses the last (rightmost) element of the list as an initial accumulator value. Our previous examples all become simplified.

add_all x = foldr1 (+) x
mul_all x = foldr1 (*) x
join_all x = foldr1 (++) x
all_true x = foldr1 (&&) x

But this only works when the type of both operands of the fold function (as well as the result type) are the same.

foldr1           :: (a -> a -> a) -> [a] -> a
foldr1 f [x]     =  x
foldr1 f (x:xs)  =  f x (foldr1 f xs)
foldr1 _ []      =  error "Prelude.foldr1: empty list"

foldr1 is particularly useful when the fold function has a natural right identity element i such that

f x i = x

Similarly foldl1 uses the leftmost element as the initial accumulator value, working well when the folded function has a natural left identity element.

List Comprehensions

Updated Sat Sept. 01 2018, 18:24 by cameron.