Can we solve this equation for X ?
Applicative is to monad what X is to comonad
To me it seems that Apply
class should not be a part of the picture at all.
For example the definition of apply
in @Zeta's answer does not seem to be well-behaved. In particular, it always discards the context of the first argument and only uses the context of the second argument.
Intuitively, it seems that comonad is about "splitting" the context instead of combining, and so "co-applicative" should be the same.
This question seems to have better answers: Is there a concept of something like co-applicative functors sitting between comonads and functors?.
After giving it some thought, I think this is actually a backward question. One might think that ComonadApply is to Comonad
what Applicative
is to Monad
, but that is not the case. But to see this, let us use PureScript's typeclass hierarchy:
class Functor f where
fmap :: (a -> b) -> f a -> f b
class Functor f => Apply f where
apply :: f (a -> b) -> f a -> f b -- (<*>)
class Apply f => Applicative f where
pure :: a -> f a
class Applicative m => Monad m where
bind :: m a -> (a -> m b) -> m b -- (>>=)
-- join :: m (m a) -> m a
-- join = flip bind id
As you can see, ComonadApply
is merely (Apply w, Comonad w) => w
. However, Applicative
's ability to inject values into the functor with pure
is the real difference.
The definition of a Comonad
as the categorical dual consists of return
's dual extract
and bind
's dual extend
(or the alternative definiton via duplicate
as join
's dual):
class Functor w => Comonad w where
extract :: w a -> a
extend :: (w a -> b) -> w a -> w b
-- extend f = fmap f . duplicate k
-- duplicate :: w a -> w (w a)
-- duplicate = extend id
So if we look at the step from Applicative
to Monad
, the logical step between would be a typeclass with pure
's dual:
class Apply w => Extract w where
extract :: w a -> a
class Extract w => Comonad w where
extend :: (w a -> b) -> w a -> w b
Note that we cannot define extract
in terms of extend
or duplicate
, and neither can we define pure
/return
in terms of bind
or join
, so this seems like the "logical" step. apply
is mostly irrelevant here; it can be defined for either Extract
or Monad
, as long as their laws hold:
applyC f = fmap $ extract f -- Comonad variant; needs only Extract actually (*)
applyM f = bind f . flip fmap -- Monad variant; we need join or bind
So Extract
(getting values out) is to Comonad
what Applicative
(getting values in) is to Monad
. Apply
is more or less a happy little accident along the way. It would be interesting whether there are types in Hask that have Extract
, but not Comonad
(or Extend
but not Comonad
, see below), but I guess those are rather rare.
Note that Extract
doesn't exist—yet. But neither did Applicative
in the 2010 report. Also, any type that is both an instance of Extract
and Applicative
automatically is both a Monad
and a Comonad
, since you can define bind
and extend
in terms of extract
and pure
:
bindC :: Extract w => w a -> (a -> w b) -> w b
bindC k f = f $ extract k
extendM :: Applicative w => (w a -> b) -> w a -> w b
extendM f k = pure $ f k
* Being able to define apply
in terms of extract
is a sign that class Extend w => Comonad w
could be more feasible, but one could have split Monad
into class (Applicative f, Bind f) => Monad f
and therefore Comonad
into (Extend w, Extract w) => Comonad w
, so it's more or less splitting hair.