Could someone please explain to me, what is the purpose of the typeclass Traversable
?
The typeclass definition is:
class (Functor t, Folda
The result type of the application f a has to be an applicative, why? Would a functor not be enough?
This is a fantastic question. The original McBride & Paterson paper goes in the other direction: it notices that a lot of computation are applicative in nature (can be rewritten with pure
and <*>
). Then it notices that certain containers, like []
, allow for a function of this type:
idist :: Applicative f => [f a] -> f [a]
idist = ...
This we now call sequence
in the Traversable
class. All well and good, but it helps to probe the strength of our assumptions when we write abstractions. What if we tried to build a traversable library without Applicative
, using only Functor
? What exactly would go wrong?
For this it helps to read through the Jaskelioff & Rypacek paper that tries to pin down structures in category theory that correspond to applicative functors and traversable containers. The most interesting property of traversable containers is that they are closed under finite sums and products. This is great for Haskell programming, where a vast number of datatypes can be defined with sums and products:
data WeirdSum a = ByList [a] | ByMaybe (Maybe a)
instance Traversable WeirdSum where
traverse a2fb (ByList as) =
ByList <$> traverse a2fb as
traverse a2fb (ByMaybe maybeA) =
ByMaybe <$> traverse a2fb maybeA
Ah, more evidence that we do not need all the power of Applicative! We are only using fmap
here. Now finite products:
data WeirdProduct a = WeirdProduct [a] (Maybe a)
instance Traversable WeirdProduct where
traverse a2fb (WeirdProduct as aMaybe) =
WeirdProduct <$> traverse a2fb as <*> traverse a2fb aMaybe
Here it is impossible to write a definition with just functors: fmap
is great for sums but gives us no way to "glue" together two different functorial values. It is only with <*>
that we are able to "close" traversable containers over finite products.
This is all well and good but lacks precision. We are sort of cherry-picking evidence here that Functor
might be bad, but could we argue from first principles that Applicative
is exactly what we need, no more and no less?
This problem is tackled in the second half of the Jaskelioff & Rypacek paper. In category-theoretic terms, a functor T
is traversable iff it allows for a family of natural transformations
{ sequence | sequence : TFX -> FTX, any applicative F }
where each natural transformation is "natural in F" and respects the "monoidal structure of applicative functor composition." It is that last phrase, that last little piece of jargon, where it is important to have Applicative
rather than Functor
. With Applicative f
, we are able to glue together values of type f a
and f b
, where we either act on them (a la foo <$> fa <*> fb
where foo :: a -> b -> c
and fa, fb :: f a, f b
) or just shove them into a tuple f (a, b)
. This gives rise to the aforementioned "monoidal structure"; we need this to then prove that traversable functors are closed over finite products, just like we showed above. Without applicatives we couldn't even begin talking about how functors and products interact! If Hask is our category of Haskell types, then an applicative is just a way to name Hask-to-Hask endofunctors that "behave well" around (->)
types and product types.
Hopefully this two-pronged answer, one in practical programming and one in categorical foo-foo, gives a little intuition as to why you want applicative functors when talking about traversability. I think often traversables are introduced with an element of magic around them, but they are very much motivated by practical concerns with solid theoretical foundations. Other language ecosystems may have easier-to-use iteration patterns and libraries, but I for one love the simplicity and elegance of traverse
and sequence
.