Lecture 21: Foldable and Traversable
Foldable
We've been using the foldr function to crunch lists, as if its type was
foldr :: (a -> b -> b) -> b -> [a] -> b
This is great, but there are other collection classes that order their elements, and so could be processed in conceptually the same way. To that end, we have the Foldable type:
class Foldable t where
foldr :: (a -> b -> b) -> b -> t a -> b
...
which is where foldr actually lives. Let's do a simple example another type that orders its elements, the BinaryTree type we've seen before:
data BinaryTree a
= Empty
| Node (BinaryTree a) a (BinaryTree a)
deriving Show
We can write foldr for this type:
instance Foldable BinaryTree where
foldr f acc Empty = acc
foldr f acc (Note left a right) =
foldr f (f a (foldr f acc right)) left
The equation for foldr f acc (Note left a right) takes a little thinking, but this form is easy to derive if you understand that foldr builds its result value from right-to-left.
The Foldable type class defines a number of other useful functions, e.g.,
class Foldable t where
foldr :: (a -> b -> b) -> b -> t a -> b
foldr1 :: (a -> a -> a) -> t a -> a
foldl :: (b -> a -> b) -> b -> t a -> b
foldl1 :: (a -> a -> a) -> t a -> a
fold :: Monoid m => t m -> m
foldMap :: Monoid m => (a -> m) -> t a -> m
toList :: t a -> [a]
...
The foldl function does a left-to-right crunching of the list, and is often useful in processing very large data structures that are built lazily. The variants foldr1 and foldl1 deal with simple, monoid-like folds over non-empty lists (obviating the need for an explicit identity).
The Foldable type class includes many other functions we've been thinking of as list-specific, e.g., length, elem, minimum, maximum, sum, and product. A moment's reflection will reveal that each can easily be written in terms of foldr, and so their greater generality should not be a surprise.
The fold and foldMap functions deal with the case where the combining function has type m -> m -> m for some Monoid m. Default definitions exist so that a minimal complete definition of a Foldable instance must define either foldr or foldMap. What is perhaps even more surprising is that it is often easier to write the instance definition using foldMap, e.g.
instance Foldable BinaryTree where
foldMap _ Empty = mempty
foldMap f (Node left a right) =
foldMap f left <> f a <> foldMap f right
This is very natural: we process the recursive parts recursively, and combine the pieces using (<>).
What may be surprising in all of this is that functions like foldr and foldl may have different execution characteristics, but because foldr is complete for Foldable, it is somehow possible to write foldl in terms of foldr. How can this be? It is useful to understand the definition of foldMap in terms of foldr, and conversely.
To get foldMap from foldr is relatively straightforward:
foldMap f container = foldr (\a b -> f a <> b) mempty container
Exercise 21.1 The code fairy would like you to η-reduce the definition above. She doesn't want you to eliminate f, but she thinks you should be able to eliminate a, b, and container without difficulty.
What is perhaps surprising is the other direction. The foldMap function requires an argument whose domain is a Monoid, but there's no such restriction on the combining function of foldr. Obviously, there's some sort of trick. A crucial observation is that if f is the combining function for foldr, then providing a single argument to f results in a function of type b -> b, and this may remind you of the Endo type:
newtype Endo a = Endo { appEndo :: a -> a }
The type Endo a is a monoid instance, where (<>) is just composition under the wrapper, and mempty is Endo id. Thus,
foldr f acc container = appEndo (foldMap (\a -> Endo (f a)) container) acc
i.e., we use foldMap to build a function, which, when applied to acc, reduces to foldr f acc container.
Exercise 21.2 It's the code fairy again. Do I need to tell you what to do?
This may (should?) remind you of the difference list approach to implementing the Doc type from Scalability supplementary lecture (16a).
Traversable
The Traverable type class includes types that we can "walk across." It is a simultaneous generalization of Functor and Foldable, and as we'll see, it's especially natural and easy to implement when the underlying type is also an Applicative instance.
Let's begin with a reconsideration of the Applicative type class, which we sometimes think of as a generalization of the Functor type class that enables us to apply pure functions to zero or more fancy values, where what "apply" means depends on particular Applicative type. It is useful to think of Applicative types as defining a kind of idiom.
Consider, for example, the function mapM :: Monad m => (a -> m b) -> [a] -> m [b]. We can implement this function by "walking along the list," i.e., via a natural structural induction, as follows:
mapM f [] = pure []
mapM f (a:as) = pure (:) <*> f a <*> mapM as
McBride and Patterson, the authors of the original paper on Applicative, invented the idea of "idiom brackets," where
⟦ f a_1 ... a_n ⟧
represents
pure f <*> a_1 <*> ... <*> a_n
If we rewrite the defining of mapM using idiom brackets, we have
mapM f [] = ⟦ [] ⟧
mapM f (a:as) = ⟦ (:) (f a) (mapM f as) ⟧
Contrast this, for a moment, with the definition of map:
map f [] = []
map f (a:as) = f a : map f as
If we rewrite the last line in prefix notation, we get
map f [] = []
map f (a:as) = (:) (f a) (map f as)
Thus, the definition of mapM is just the definition of map, but embellished with idiom brackets on the right-hand side. Another way of describing this is that mapM is an effectful version of map, or alternatively, that map is a pure version of mapM.
This may seem like abstract nonsense, but it makes an interesting point: mapM has the wrong constraint! We didn't need Monad, only Applicative. We'll see in a bit why this hasn't been fixed.
Our next thought about generalizing mapM is that we might be able to replace the [] type with a less restrictive type class. This leads us to
class (Functor t, Foldable t) => Traversable t where
traverse :: Applicative f => (a -> f b) -> t a -> f (t b)
Note that traverse is mapM, but with just the right type constraints: we've moved from Monad to Applicative as the constraint on the type of f, and we're generalizing [] to t. In fact, the implementation of mapM in the Haskell sources is just mapM = traverse!
But the curious thing is that creating instances of Traversable follows the pattern we established above for mapM. We just “implement fmap,” and then put the left hand side of the definitions in idiom brackets, i.e., add pure and (<*>) as needed.
Let's do an extended example, using the BinaryTree type as before. As we'll see, this involves an interesting and unexpected difficulty.
We have the Foldable instance above, and we've done Functor before:
instance Functor BinaryTree where
fmap _ Empty = Empty
fmap f (Node left a right) =
Node (fmap f left) (f a) (fmap f right)
Let's remember that definition!
The traverse function is just an effectful fmap, intuitively,
instance Traversable BinaryTree where
traverse _ Empty = [[ Empty ]]
traverse f (Node left a right) =
[[ Node (traverse f left) (f a) (traverse f right) ]]
I.e., just the definition of fmap for this type, but with idiom brackets on the left. Of course, idiom brackets aren't part of Haskell syntax, so we have to translate them out:
instance Traversable BinaryTree where
traverse _ Empty = pure Empty
traverse f (Node left a right) =
Node <$> traverse f left <*> f a <*> traverse f right
Of course, it's one thing to have this toy, and another to know how to play with it. Let's consider a simple problem: labelling the items in a container with an integer. To that end, we'll use a little gadget in State:
label :: a -> State Int (Int,a)
label a = do
ix <- get
modify (+1)
pure (ix,a)
We can then add labels to the values held in a Traversable container:
addLabels :: (Traversable t) => t a -> t (Int,a)
addLabels ta = evalState (traverse label ta) 1
With this:
> testTree
Node (Node Empty
1
(Node Empty 2 Empty))
3
(Node Empty
4
(Node Empty 5 Empty))
> addLabels testTree
Node (Node Empty
(1,1)
(Node Empty (2,2) Empty))
(3,3)
(Node Empty
(4,4)
(Node Empty (5,5) Empty))
> toList (addLabels testTree)
[(1,1),(2,2),(3,3),(4,4),(5,5)]
exactly as you might expect.
*Exercise 21.3 The throwaway use of State here is a bit heavy-handed. Data.Traversable includes two functions, mapAccumL and mapAccumR that perform state-based traversals, albeit with an explicitly exposed state. Rewrite addLabel using one of the mapAccum{L,R} functions, rather than State.
Exercise 21.4 Let's consider a different tree-based container class:
data MultiTree a
= Node [MultiTree a]
| Leaf a
This describes a tree, in which the values are contained in Leaf values, and the Node values can contain an arbitrary number of sub-trees.
Implement the usual type classes for MultiTree: Eq , Ord, Functor, Applicative, Monad, Monoid (for MultiTree a), Alternative, MonadPlus, Foldable, Traversable, at suitable kinds and with suitable constraints.
If you do this artfully, you'll see sub-expressions like fmap . fmap in the definition of fmap, and traverse . traverse in the definition of traverse. These happen because [] is a Functor and a Traversable, and both Functor and a Traversable are closed under composition.
The Traversable type class is also the homes of another familiar function, sequence, which until recently had the type:
sequence :: Monad m => [m a] -> m [a]
In Traversable, the specific use of lists is revised to account for any Traversable type f:
sequence :: Monad m => t (m a) -> m (t a)
but here, as in the case of mapM, the implementation doesn't require the full power of a Monad constraint, and so we also have:
sequenceA :: Applicative f => t (f a) -> f (t a)
with
sequence = sequenceA
being merely a less generally typed version of sequenceA.
A Bit of Wisdom
There is a programming truism, evidently due to Tony Hoare but often attributed to Donald Knuth that's well worth knowing: Premature optimization is the root of all evil. There is considerable wisdom in this, but also a history of it being applied foolishly.
For functional programmers, eschewing premature optimization often involves a natural preference for lists as an all-purpose container class, even when they're not entirely appropriate. Taking efficiency into consideration during the design phase of a program is timely, not premature, optimization, even if lower-level optimization is not. But we can usually get away with what otherwise would be a poor choice: by writing list-based code on a first pass, and then generalizing it to be type class dependent rather than specifically list dependent, gradually decoupling our program's code from it's data representation choices, and so making it possible to revisit those choices late in program development, at a point when it would be positively painful in traditional languages.
A Bit of Whimsy
One of the standard arguments for preferring Applicative over Monad whenever possible is that the composition of Applicative types are also Applicative in a natural way, whereas the composition of Monad types is not necessarily a Monad.
Recall that category-theoretic monads are defined in terms of join :: (Monad m) => m (m a) -> m a. The type of sequenceA hints at the fundamental difficulty. If we had a natural operator commuteM :: (Monad m, Monad n) => m (n a) -> n (m a), then we'd have a natural definition of join for the composed monads, as follows.
We start by undefining what we can't define (am I the only one who recalls the disassociate press version of the Gettysburg Address?!):
commuteM :: (Monad m, Monad n) => m (n a) -> n (m a)
commuteM = undefined
This may seem pointless, but we want to show that this is the only obstruction, which is remarkable given the parallels with sequenceA: all we're missing is the Traversable instance for m!
Our goal is to show the we can start with m (n (m (n a))), and end with m (n a).
This sweeps under the rug a bit of the type traffic (we use Data.Functor.Compose's Compose type to represent the composition, and this requires unwrapping and re-wrapping), but gives the main idea.