What should a Traversable instance look like for a Tree datatype with a nested Maybe value?

traverse lets you apply a "function with an effect" to every "slot" of a data structure, maintaining the structure's shape. It has the type:

traverse :: Applicative f => (a -> f b) -> t a -> f (t b)

It relies crucially on the fact that the type of the "effects" is an Applicative. What operations does Applicatve provide?

  • it lets us lift pure functions and apply them to effectful actions with <$>.
  • it lets us combine effectful actions with (<*>) :: f (a -> b) -> f a -> f b. Notice that the second parameter is an effectful action, not a pure value.
  • it lets us take any pure value and put it in an effectful context, using pure :: a -> f a.

Now, when the node has a Nothing, there's no effect to perform because there aren't any values, but the <*> still requires an effectful action on the right. We can use pure Nothing to make the types fit.

When the node has a Just t, we can traverse the subtree t of type Tree a with the function a -> f b and end up with an action f (Tree b). But the <*> is actually expecting an f (Maybe (Tree b)). The lifted Node constructor makes us expect that. What can we do?

The solution is to lift the Just constructor into the action using <$>, which is another name for fmap.

Notice that we haven't changed the overall "shape" of the value: the Nothing is still Nothing, the Just is still Just. The structure of the subtrees didn't change either: we traversed them recursively but didn't modify them otherwise.


The short answer is that you need to use traverse to get inside the Maybe.

The Traversable and Foldable instances for a type often have a similar structure to its Functor instance. Whereas fmap maps a pure function over a structure, combining the results back up with the pure constructors:

instance Functor Tree where
  fmap f (Leaf1 a) = Leaf1 (f a)
  fmap f (Leaf2 a1 a2) = Leaf2 (f a1) (f a2)
  fmap f (Node ta mta) = Node (fmap f ta) (fmap (fmap f) mta)

Note the (fmap (fmap f) mta): the outer fmap maps over the Maybe, while the inner one maps over the Tree:

(fmap
  :: (Tree a -> Tree b)
  -> Maybe (Tree a) -> Maybe (Tree b))
  ((fmap
    :: (a -> b)
    -> Tree a -> Tree b)
    f)
  mta

traverse instead maps an effectful function over the structure, and correspondingly lifts the constructors into Applicative with the <$> and <*> operators:

instance Traversable Tree where
  traverse f (Leaf1 a) = Leaf1 <$> f a
  traverse f (Leaf2 a1 a2) = Leaf2 <$> f a1 <*> f a2
  traverse f (Node ta mta) = Node <$> traverse f ta <*> traverse (traverse f) mta

Again, notice that we must traverse the Maybe, and within that, traverse the Tree, but instead of a pure function a -> b, we just have an effectful function a -> f b, given Applicative f:

(traverse
  :: (Tree a -> f (Tree b))
  -> Maybe (Tree a) -> f (Maybe (Tree b)))
  ((traverse
    :: (a -> f b)
    -> Tree a -> f (Tree b))
    f)
  mta

Likewise, foldMap has a similar structure, but instead of reconstructing the data type, it combines results using a Monoid instance:

instance Foldable Tree where
  foldMap f (Leaf1 a) = f a
  foldMap f (Leaf2 a1 a2) = f a1 <> f a2
  foldMap f (Node ta mta) = foldMap f ta <> foldMap (foldMap f) mta

And here’s a simple example usage of traverse:

> traverse (\ x -> print x *> pure (x + 1)) (Node (Leaf1 10) (Just (Leaf2 20 30)))
10
20
30
Node (Leaf1 11) (Just (Leaf2 21 31))

With the DeriveFoldable, DeriveFunctor, and DeriveTraversable extensions, you may add a deriving (Foldable, Functor, Traversable) clause to a data type and use the -ddump-deriv flag of GHC to see the generated code.