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.