Why are traversals defined over Applicatives, fundamentally?

410 views Asked by At

I've been on a bit of a "distilling everything to its fundamentals" kick lately, and I've been unable to find clear theoretical reasons for how the Traversable typeclass is defined, only practical ones of "it's useful to be able to traverse over applicative coalgebras, and lots of datatypes can do it" and a whole lot of hints.

I'm aware that there's an applicative "family", as described by https://duplode.github.io/posts/divisible-and-the-monoidal-quartet.html.

I'm also aware that while Traversable traversals are applicative coalgebras, the Traversable1 typeclass from 'semigroupoids' describes apply coalgebras, and the Distributive typeclass from 'distributive' describes functor algebras.

Additionally, I'm aware that Foldable, Foldable1, and theoretical fold family members, describe datatypes that can be folded using monoids, semigroups, and corresponding monoid family members such as magmas (for folding as a binary tree) and commutative versions of each (for folding as unordered versions of each).

As such, as Traversable is a subclass of Foldable, I assume it's monoidal in nature, and similarly I assume Traversable1 is semigroupal in nature, and Distributive is comonoidal in nature (as mentioned in its description in the 'distributive' package).

This feels like the right track, but where do Applicative and Apply come from here? Are there magmatic and commutative versions? Would there be a distributive family in a category with non-trivial comonoids?

Essentially, my question is "do these typeclasses exist, and what are they? if not, why not?":

class FoldableMagma t => TraversableMagma t where
    traverseMagma :: ??? f => (a -> f b) -> (t a -> f (t b))
class FoldableCommute t => TraversableCommute t where
    traverseCommute :: ??? f => (a -> f b) -> (t a -> f (t b))
class Foldable t => ContraTraversable t where
    contraTraverse :: Divisible f => (b -> f a) -> (t a -> f (t b))
-- im really not sure on this last one
-- but it's how i'd expect an endofunctor over coalgebras to look
-- which seems potentially related to traversables?

Presumably less important bonus question: while attempting to research this, I came across the 'data-functor-logistic' package https://hackage.haskell.org/package/data-functor-logistic

This describes a version of Distributive over contravariant functors - is there an equivalent Traversable over Divisibles (or Decidables)?

2

There are 2 answers

3
UltimateMath101 On BEST ANSWER

I'm not aware of any library that implements those classes, but I'll try to unravel what those classes would represent. I am a programmer, not a category theorist, so take this with a grain of salt.

Applicative variants

ApplyMagma

The ApplyMagma class has exactly the same methods as the Apply class, but it doesn't need to follow the associativity law.

class Functor f => ApplyMagma f where
    (<.>) :: f (a -> b) -> f a -> f b

If Apply is analogous to semigroups, ApplyMagma is analogous to magmas.

ApplyCommute

The ApplyCommute class will be equivalent to the Apply class but with the following commutativity law:

f <$> x <.> y = flip f <$> y <.> x

If Apply is analogous to semigroups, ApplyCommute is analogous to commutative semigroups.

Traversable1 variants

Traversable1Magma

A Traversable1Magma can be seen as a Traversable1 with more information provided about the structure. While the Foldable1 class has a toNonEmpty method, The Foldable1Magma class could have a toBinaryTree method.

class (FoldableMagma t, Traversable1 t) => Traversable1Magma t where
    traverseMagma :: ApplyMagma f => (a -> f b) -> (t a -> f (t b))

Traversable1Commute

A Traversable1Commute can be seen as a Traversable1 without a defined ordering to the elements. If it didn't require an Ord a constraint, Set from containers could be an instance of this class. Traversable1Commute could be a superclass of Traversable1.

class (FoldableCommute t, Functor t) => Traversable1Commute t where
    traverseCommute :: ApplyCommute f => (a -> f b) -> (t a -> f (t b))

Note that these are variants of Traversable1 because neither ApplyMagma nor ApplyCommute have a function equivalent to pure.

ContraTraversable

ContraTraversable does not have any instances. To see why, look at the type of the contraTraverse function.

contraTraverse :: Divisible f => (b -> f a) -> (t a -> f (t b))

We can specialize this to the following:

contraTraverse :: Monoid b => (b -> Op b a) -> (t a -> Op b (t b))

Which is eqivalent to the following:

contraTraverse ~ Monoid b => (b -> a -> b) -> t a -> t b -> a

Using const and the conquer function from Divisible, this allows us to create a value of any type, which is impossible.

0
janet On

Since asking this and receiving the previous (excellent) answer, I have learnt another reason why Applicative is used: algebraic datatypes!

Without any constraint, it can only describe uninhabited datatypes, like this:

data V1 a
instance VTraversable V1 where
    vtraverse _ = \case
-- uninhabited types can be pattern matched to create any result type

If it were Functor, it could describe algebraic datatypes that look something like these:

data FTrav1 a = FTrav1 a
instance FTraversable FTrav1 where
    ftraverse strat (FTrav1 a) = FTrav1 <$> strat a

data FTrav2 a = FTrav2_1 a | FTrav2_2 (FTrav1 a)
instance FTraversable FTrav2 where
    ftraverse strat (FTrav2_1 a) = FTrav2_1 <$> strat a
    ftraverse strat (FTrav2_2 fa) = FTrav2_2 <$> ftraverse strat fa

Essentially, it's any datatype with an arbitrary (potentially infinite, if that were describable in Haskell) number of constructors of a single FTraversable argument (where a ~ Identity a). This is saying that any Traversable f a is isomorphic to (f (), a), the Writer functor.

Introducing Apply enables extra datatypes like the following:

data ApplyTrav1 a = ApplyTrav1 a a
instance Traversable1 ApplyTrav1 where
    traverse1 strat (ApplyTrav1 a a) = ApplyTrav1 <$> strat a <*> strat a

data ApplyTrav2 a = ApplyTrav2_1 a (ApplyTrav1 a) | ApplyTrav2_2 (ApplyTrav1 a) a
instance Traversable1 ApplyTrav2 where
    traverse1 strat (ApplyTrav2_1 a fa) = ApplyTrav2_1 <$> strat a <*> traverse1 strat fa
    traverse1 strat (ApplyTrav2_2 fa a) = ApplyTrav2_2 <$> traverse1 strat fa <*> traverse1 strat a

Now constructors can have arbitrarily many arguments, as long as it's a finite number greater than zero! The isomorphism is now to (f (), NonEmpty a), where they are of equal sizes.

Applicative enables the following:

data ApplicTrav a = ApplicTrav0 | ApplicTrav a a
instance Traversable ApplicTrav where
    traverse _ ApplicTrav0 = pure ApplicTrav0
    traverse strat (ApplicTrav a a) = ApplicTrav <$> strat a <*> strat a

Now empty constructors are allowed! The isomorphism is now to (f (), [a]).

A hypothetical commutative Applicative would be used for commutative algebraic datatypes, were they a thing - perhaps, if Set enforced that it were only foldable with commutative monoids, they would be relevant! But to my knowledge, commutative datatypes are not a core part of Haskell, and so this form of traversal will not show up for algebraic datatypes.

Distributive is similar, it describes functors with a single constructor of arbitrarily many records (potentially infinite).

Logistic is unrelated to this algebraic interpretation, to my knowledge - since the Reader functor is commonly used with Distributives to create a collection of getter functions, Logistic is designed to work with the Op contravariant functor to create a collection of setter functions.

This suggests to me that the equivalent for Traversable doesn't exist, due to the Writer functor that characterises Traversables being its own opposite ((r,a) is isomorphic to (a,r)).