The purpose of the Traversable typeclass

后端 未结 3 1085
梦毁少年i
梦毁少年i 2021-02-19 09:56

Could someone please explain to me, what is the purpose of the typeclass Traversable?

The typeclass definition is:

class (Functor t, Folda         


        
相关标签:
3条回答
  • 2021-02-19 09:59

    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?

    Products!

    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?

    Category theory!

    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.

    0 讨论(0)
  • 2021-02-19 10:06

    Traversable in Haskell unifies the concept of mapping over a container (getting a similary-shaped container in return) with the concept of "internal iterator" that performs an effect for each element.

    Compared to external iterators, internal iterators are constrained in that we can't use a value obtained for one element to decide what to do with other elements. We can't say "mmmm, if the operation returns 7 for some element, launch the missiles when processing the next one".

    This type of "rigid" computations, that can't change course based on values determined mid-way, is represented in Haskell by the Applicative typeclass. That's the reason Traversable (the containers) and Applicative (the effects) go hand-in-hand. Functor is not enough because it doesn't provide a method to combine effectful actions.

    Allowing any kind of Applicative effect is a boon; it means we can traverse a container performing IO, existing early from failure, collecting log messages, collecting error messages from failed iterations, iterating concurrently... or any combination of those effects.

    0 讨论(0)
  • 2021-02-19 10:07

    Identity is a bit of a poor example as it always contains exactly one value. You're right – in this case, a Functor f constraint would be sufficient. But clearly, most traversables aren't so structurally trivial.

    What traverse does is: it “visits”, in some well-specified order, all elements in a container, performs some operation on them, and reconstructs the structure as it was. This is more powerful than either

    • Functor t, which also allows you to visit/modify all elements and reconstructs the structure, but only completely independent of one another (thus allowing to choose an arbitrary order-of-computation, returning a thunk to the structure before any of the elements have been (lazily) mapped at all, etc.).
    • Foldable t, which brings the elements in a linear order, but does not reconstruct the structure. Basically, Foldable is just the class of containers that can be demoted to a simple list, as witnessed by

      toList :: Foldable t => t a -> [a]
      

      ...or to a concatenation of any monoidal type, via

      foldMap :: (Foldable t, Monoid m) => (a -> m) -> t a -> m
      

      Here, the results of the operation on each element are combined through the monoid operation (or, in case the are no elements, the result is mempty).

    In case of traverse, the Applicative f constraint basically lifts this monoid-combining to something in which you can also reconstruct the structure. The correspondence is

    mempty      ::   m
    pure mempty :: f m
    

    and

    (<>)        ::   m ->   m ->   m
    liftA2 (<>) :: f m -> f m -> f m
    

    ...but in addition, because f is also a functor, you can wrap the local results in any data constructor and thus build not only a generic list-like thing but an arbitrary container, including one with the original structure.

    0 讨论(0)
提交回复
热议问题