In this answer (which I ideally should have written some time before the first anniversary of the question, but alas), I will mostly stick to covariant monoidal functors in order to keep the scope manageable, though there will be opportunity to consider some of the other aspects of the question and of your self-answer.
Another editorial choice of mine will be exercising restraint in using "co-" to name things. That's not just because it's easy to get lost in a soup of prefixes when, as you note, there is a baseline of sixteen hypothetical monoidal functor classes, but also because for each of them there generally is more than one plausible candidate for the "co-" moniker. For instance, consider Applicative
(cast into a by-the-book monoidal functor signatures):
class Functor f => Applicative f where
zipped :: (f a, f b) -> f (a, b)
unit :: () -> f ()
-- zipped = uncurry (liftA2 (,))
-- unit = const (pure ())
One might want to adopt as "coapplicative" its straightforward oplax monoidal counterpart, obtained by switching from Hask to Haskop (and thus turning around the arrows of the methods) while keeping (,)
as the tensor product:
class Functor f => Unzip f where
unzip :: f (a, b) -> (f a, f b)
trivialU :: f () -> ()
These combinators have a lawful implementation for every Functor
, with unzip = fmap fst &&& fmap snd
and trivialU = const ()
. Notably, this is the coapplicative alluded to by the distributive documentation when it notes that:
Due to the lack of non-trivial comonoids in Haskell, we can restrict ourselves to requiring a Functor rather than some Coapplicative class.
Besides the triviality of Unzip
, another plausible objection to calling it coapplicative is that a thorough dualisation of applicative should also replace (,)
(the product in Hask) with Either
(the product in Haskop). Doing so leads to your Matchable
class, which I'll display here in a presentation borrowed from a post by Conor McBride :
class Functor f => Decisive f where
orwell :: f (Either a b) -> Either (f a) (f b)
nogood :: f Void -> Void
(It is worth noting that similar questions arise when switching from covariant to contravariant functors. While replacing Functor
with Contravariant
in the signatures of Applicative
leads to Divisible
, if the tensor products are also dualised in a compatible way we end up with Decidable
instead. Furthermore, as argued in my Divisible and the monoidal quartet blog post, Decidable
mirrors Applicative
more closely than Divisible
.)
While at first it might look like only functors holding exactly one value can be Decisive
, that only holds if we require orwell
to be an isomorphism. In particular, as McBride highlights, any comonad (or really, any functor with something to play the role of extract
) can be made Decisive
:
orwellW :: Comonad w => w (Either a b) -> Either (w a) (w b)
orwellW u = case extract u of
Left a -> Left $ either id (const a) <$> u
Right b -> Right $ either (const b) id <$> u
nogoodW :: Comonad w => w Void -> Void
nogoodW = extract
orwellW
uses the result of extract
as a default value, in order to redact one of the Either
cases. For demonstrative purposes, here is how it might be deployed in implementing a filter-resembling operation which, instead of merely removing rejected, replaces them with a default:
import Data.List.NonEmpty (NonEmpty(..))
import qualified Data.List.NonEmpty as N
redact :: (a -> Bool) -> a -> [a] -> [a]
redact p d = N.tail . either id id . orwellW . (Right d :|) . map classify
where
classify a = if p a then Right a else Left a
ghci> redact (>= 0) 0 [5,-2,3,0,-1,4]
[5,0,3,0,0,4]
In this implementation, classify
uses the predicate to set up an Either
-based cosemigroup (in other words, a lawful instance of your Split
). orwellW
lifts this cosemigroup to non-empty lists (assuming that, for the sake of consistency, p d
is True
).
(redact
, by the way, also brings to mind how a padding zip can be implemented through a suitable Applicative
instance for non-empty lists, as demonstrated in this answer, incidentally also written by McBride, though at the moment I'm not sure about how deep the relationship really is.)
On the connections between Decisive
/Matchable
, Traversable
and Distributive
, I believe they boil down to all functors holding exactly one value (the left adjoint Hask endofunctors, or your FTraversable
) being comonads, and thus Decisive
. As far as these are concerned, the less powerful distributive you propose would ultimately be just FTraversable
(pushing a left adjoint inside, rather than pulling a right adjoint outside), and right now I don't see how it might generalise to Decisive
.
Now let's look at Alternative
. A by-the-book monoidal functor presentation might be:
class Functor f => Alternative f where
combined :: (f a, f b) -> f (Either a b)
nil :: () -> f Void
-- combined = uncurry (<|>) . bimap (fmap Left) (fmap Right)
-- nil = const empty
Since we'll need it in a little while, it's worth emphasising we can recover (<|>)
by lifting the trivial Either
-based monoid:
-- either id id :: Either a a -> a
aplus :: (f a, f a) -> f a
aplus = fmap (either id id) . combined
The straightforward oplax counterpart to Alternative
happens to be something familiar:
class Functor f => Filterable f where
partition :: f (Either a b) -> (f a, f b)
trivialF :: f Void -> ()
This is actually equivalent to the familiar, mapMaybe
-based Filterable
class, as covered in detail by Is every Alternative Monad Filterable?. In particular, using the predicate cosemigroup lifting illustrated before with redact
leads directly to filter
.
We haven't dualised the tensors yet, though. Doing so leads to your Bias
(and Biasable
, but I'm giving up on the identity in order to have inhabited types):
class Functor f => Bias f where
biased :: f (a, b) -> Either (f a) (f b)
Lifting the trivial (,)
-based comonoid gives us:
biasing :: f a -> Either (f a) (f a)
biasing = biased . fmap (id &&& id)
Bias
offers, at least, a classifier of shapes (whether biasing
gives out Left
or Right
depends on the functorial shape of its argument), plus an associated choice between pair components. I say "at least" because dropping the identity leaves Bias
with just an associativity law, and said law doesn't rule out changes or rearrangements of the f
-shape, as long as they are idempotent and preserve the Left
/Right
classification.
If Bias
being so light on laws is deemed a problem, there is one plausible way of tightening it up somewhat. In order to present it, let's go back to Applicative
and Alternative
for a moment. While in the monoidal functor formulation Alternative
is in principle independent from Applicative
, there are a few other possible laws that are sometimes invoked to connect the two classes and better delineate the meaning of Alternative
(for commentary on that, see Antal Spector-Zabusky's answer to Confused by the meaning of the 'Alternative' type class and its relationship to other type classes). One of these laws is right distributivity of (<*>)
over (<|>)
:
(u <|> v) <*> w = (u <*> w) <|> (v <*> w)
We can translate that to the vocabulary we're using here...
zipped (aplus (u, v), w) = aplus (zipped (u, w), zipped (v w)
... and make it pointfree, for the sake of easier manipulation:
zipped . first aplus = aplus . bimap zipped zipped . distribR
distribR :: (((a, b), c) -> ((a, c), (b, c))
distribR ((a, b), c) = ((a, c), (b, c))
Now, if Bias
is dual to Alt
(Alternative
sans identity) and Decisive
is dual to Applicative
, we should be able to dualise the distributivity law for a functor which is both Decisive
and Bias
:
first biasing . orwell = codistribR . bimap orwell orwell . biasing
codistribR :: Either (Either a c) (Either b c) -> Either (Either a b) c
codistribR = \case
Left (Left a) -> Left (Left a)
Left (Right c) -> Right c
Right (Left b) -> Left (Right b)
Right (Right c) -> Right c
Adopting this law (and/or the analogous left distributivity law) would forbid biasing
(and, by extension, biased
) from changing the shape. That's because, by the identity laws of Decisive
, orwell
can't change shapes, which means, in particular:
first biasing (orwell (Right <$> u)) = Right u
Applying the distributive law then leads to:
codistribR (bimap orwell orwell (biasing (Right <$> u)) = Right u
Which is only possible if biasing
leaves the shape untouched. The distributivity law, therefore, can ensure biased
is a shape classifier with bundled with a choice between fmap fst
and fmap snd
. That it all works out so neatly highlights the correspondence not just between Alternative
/Alt
and Bias
, but also between Applicative
and Decisive
.