First of all:
Any monad is also an applicative functor and any applicative functor is a functor.
This is true in the context of Haskell, but (reading Applicative
as "strong lax monoidal functor") not in general, for the rather trivial reason that you can have "applicative" functors between different monoidal categories, whereas monads (and comonads) are endofunctors.
Further, identifying Applicative
with strong lax monoidal functors is slightly misleading, because to justify the name (and the type signature of (<*>)
) requires a functor between closed monoidal categories which preserves both the monoidal structure and the internal hom. This could plausibly be called a "lax closed monoidal functor", except that a functor between monoidal closed categories that preserves either property preserves the other in the obvious way. Because Applicative
describes only endofunctors on Hask preserving the monoidal structure of (,)
, its instances gain a lot of properties automatically, including their strength, which can thus be elided.
The apparent connection with Monad
is arguably an artifact of the implicit limitations on Applicative
causing aspects of their respective monoid structures to coincide, a happy coincidence which unfortunately does not survive dualization.
Just as a comonad on a category C is a monad on Cop, an oplax monoidal functor C→D is a lax monoidal functor Cop→Dop. But Haskop is not monoidal closed, and a co-Applicative
that doesn't include function application hardly merits the name. Anyway, the result wouldn't be terribly interesting:
class (Functor f) => CoMonoidal f where
counit :: f () -> ()
cozip :: f (a, b) -> (f a, f b)
We could instead imagine a notion of "colax closed functor", which would look much more like Applicative
if it existed. Unfortunately, Haskop is not (to the best of my knowledge) a closed category at all: newtype Op b a = Op (a -> b)
in Hask corresponds to morphisms b→a in Haskop, but Op b a
doesn't work as an internal hom there--because the arrows are reversed some sort of co-function would be required instead, which we can't define in general for Hask.
If we simply pretend that "colax closed functors" existed for Hask, and furthermore worked in the way we'd naively hope they would, a co-Applicative
based on that would probably look like this:
class (Functor f) => CoApplicative f where
copure :: f a -> a
coap :: (f a -> f b) -> f (a -> b)
Adding duplicate :: f a -> f (f a)
to copure
would produce a comonad (assuming the laws are satisfied), of course. But there's no obvious relationship between coap
--whatever it might be--and extend :: (f a -> b) -> f a -> f b
. Comparing the types it becomes clear that the dualization is happening in different ways: the comonoidal structures underlying duplicate
and cozip
have little to do with each other or with coap
(which probably doesn't make sense anyway), whereas liftA2 (,)
and (<*>)
are equivalent and can be derived from join
.
Another possible way of dualizing Applicative
, which has even less to do with comonads, is to consider contravariant monoidal functors:
class (Contravariant f) => ContraMonoidal f where
contraunit :: f a
contrazip :: f a -> f b -> f (Either a b)
But this runs afoul of the same issues as above, namely that Haskop is not a closed category. If it were, we would have some type b <~ a
such that we could write functions like contracurry :: (Either c b <~ a) -> (c <~ (b <~ a))
and contraapply :: b -> Either a (a <~ b)
and so on that actually worked as expected.
If memory serves me, the obstacles here are not specific to Haskell, but rather arise from Hask being cartesian closed (up to the usual hand-waving, of course), a property it shares with most typed lambda calculi, so you're not likely to get very far with a CoApplicative
in most settings.
However, in a monoidal closed category more hospitable to dualization you might have better luck. In particular, I believe both Kleisli (Cont r)
and its opposite category are monoidal closed, so that might be a better context to explore these ideas.