I have a Bitraversable called t that supports this operation:
someName :: Monad m => (t (m a) (m b) -> c) -> m (t a b) -> c
In other words, it's possible to take a function that accepts two monads packaged into the bitraversable and turn it into a mapping that accepts a single monad containing a bitraversable without the monad layer. This is something like a bitraversable and higher-level version of distribute; the type signature is similar to this:
\f -> \x -> f (distribute x)
:: (Distributive g, Functor f) => (g (f a) -> c) -> f (g a) -> c
My questions:
Is there a standard name for this "higher-level" version of
distributethat works on functions that accept distributives rather than distributives themselves?Is there a name for the bitraversable version?
Does it work with every bitraversable/functor/monad/whatever, or are there restrictions?
As per @Noughtmare, your "higher level" functions
someNameanddistributeare just written in continuation passing style. These generally aren't worth additional names, because they are just right function compositions:Practically speaking, anywhere you want to call
highLevelDistributeon an argument:this expression is equivalent to:
and even if you're using
highLevelDistributeas a first-class value, it's just not that hard to write and understand the section(. distribute).Note that
traverseandsequenceAare a little different, since we have:You could make an argument that this difference doesn't really warrant separate names either, but that's an argument for another day.
Getting back to
someName, it's a CPS version of:which looks like a bifunctor analogue of
distribute:So, I'd suggest inventing a
Bidistributiveto reflect this, andsomeOtherNamebecomesbidistribute:Again, your "higher level"
someNameis just right-composition:Reasonable laws for a
Bidistributivewould probably include the following. I'm not sure if these are sufficiently general and/or exhaustive:For your question #3, not all
Bitraversables areBidistributive, for much the same reason that not allTraversables areDistributive. ADistributiveallows you to "expose structure" under an arbitrary functor. So, for example, there's noDistributiveinstance for lists, because if there was, you could call:which would allow you to determine if a list returned by an IO action was empty or not, without executing the IO action.
Similarly,
EitherisBitraversable, but it can't beBidistributive, because if it was, you'd be able to use:to determine if the IO action returned a
LeftorRightwithout having to execute the IO action.One interesting thing about
bidistributeis that the "other functor" can be anyFunctor; it doesn't need to be anApplicative. So, just as we have:we have:
Intuitively, sequencing needs the power of an applicative functor
fto be able to "build" thef (t a)from a traversal of its functorialf a"parts", while distribution only needs to take thef (g a)apart. In practical terms, this means that sequencing typically looks like this:while distribution typically looks like this:
(Technically, according to the documentation for
Data.Distributive, theDistributiveclass only requires aFunctorrather than some coapplicative class because of the lack of non-trivial comonoids in Haskell. See this SO answer.)