Arrows
are generalized by Categories, and so by the Category
typeclass.
class Category f where
(.) :: f a b -> f b c -> f a c
id :: f a a
The Arrow
typeclass definition has Category
as a superclass. Categories (in the haskell sense) generalize functions (you can compose them but not apply them) and so are definitely a "model of computation". Arrow
provides a Category
with additional structure for working with tuples. So, while Category
mirrors something about Haskell's function space, Arrow
extends that to something about product types.
Every Monad
gives rise to something called a "Kleisli Category" and this construction gives you instances of ArrowApply
. You can build a Monad
out of any ArrowApply
such that going full circle doesn't change your behavior, so in some deep sense Monad
and ArrowApply
are the same thing.
newtype Kleisli m a b = Kleisli { runKleisli :: a -> m b }
instance Monad m => Category (Kleisli m) where
id = Kleisli return
(Kleisli f) . (Kleisli g) = Kleisli (\b -> g b >>= f)
instance Monad m => Arrow (Kleisli m) where
arr f = Kleisli (return . f)
first (Kleisli f) = Kleisli (\ ~(b,d) -> f b >>= \c -> return (c,d))
second (Kleisli f) = Kleisli (\ ~(d,b) -> f b >>= \c -> return (d,c))
Actually every Arrow
gives rise to an Applicative
(universally quantified to get the kinds right) in addition to the Category
superclass, and I believe the combination of the appropriate Category
and Applicative
is enough to reconstruct your Arrow
.
So, these structures are deeply connected.
Warning: wishy-washy commentary ahead. One central difference between the Functor
/Applicative
/Monad
way of thinking and the Category
/Arrow
way of thinking is that while Functor
and its ilk are generalizations at the level of object (types in Haskell), Category
/Arrow
are generelazation of the notion of morphism (functions in Haskell). My belief is that thinking at the level of generalized morphism involves a higher level of abstraction than thinking at the level of generalized objects. Sometimes that is a good thing, other times it is not. On the other-hand, despite the fact that Arrows
have a categorical basis, and no one in math thinks Applicative
is interesting, it is my understanding that Applicative
is generally better understood than Arrow
.
Basically you can think of "Category < Arrow < ArrowApply" and "Functor < Applicative < Monad" such that "Category ~ Functor", "Arrow ~ Applicative" and "ArrowApply ~ Monad".
More Concrete Below:
As for other structures to model computation: one can often reverse the direction of the "arrows" (just meaning morphisms here) in categorical constructions to get the "dual" or "co-construction". So, if a monad is defined as
class Functor m => Monad m where
return :: a -> m a
join :: m (m a) -> m a
(okay, I know that isn't how Haskell defines things, but ma >>= f = join $ fmap f ma
and join x = x >>= id
so it just as well could be)
then the comonad is
class Functor m => Comonad m where
extract :: m a -> a -- this is co-return
duplicate :: m a -> m (m a) -- this is co-join
This thing turns out to be pretty common also. It turns out that Comonad
is the basic underlying structure of cellular automata. For completness, I should point out that Edward Kmett's Control.Comonad
puts duplicate
in a class between functor and Comonad
for "Extendable Functors" because you can also define
extend :: (m a -> b) -> m a -> m b -- Looks familiar? this is just the dual of >>=
extend f = fmap f . duplicate
--this is enough
duplicate = extend id
It turns out that all Monad
s are also "Extendable"
monadDuplicate :: Monad m => m a -> m (m a)
monadDuplicate = return
while all Comonads
are "Joinable"
comonadJoin :: Comonad m => m (m a) -> m a
comonadJoin = extract
so these structures are very close together.