3
votes

I've read this Q&A but don't understand the category theory part.

Here is my reasoning so far: When I look at the types

F (a -> b) -> F a -> F b
(a -> M b) -> M a -> M b

a -> F a
a -> M a

the only portion that resembles a monoid at the type level is the type constructor, i.e. the applicative/monadic context:

// binary operation
F -> F -> F
M -> M -> M

// identity element
F
M

So I'd say applicatives/monads are monoidal in terms of their contexts, because they combine two contexts of the same type into one. pure/return creates a most minimal context, so we can think of it as the identity context similar to the identity element of a monoid, which creates a "most minimal value".

However, monads/applicatives are not monoidal in their type parameter, because they include a transformation from a to b.

I am not sure my reasoning makes any sense. What baffles me is that monoids on the one hand and applicatives/monads on the other combine things differently:

Nothing <> (Just "bar") -- Just "bar"
(++) <$> Nothing <*> (Just "bar") -- Nothing
Nothing >>= (\x -> (Just "bar") >>= (return . (++) x)) -- Nothing

However, I guess the different result values are due to monoids interpret expressions as ordinary values whereas applicatives/monads interpret expressions as computations (one that may fail in the above example).

Now in the aforementioned Q&A is stated monads are monoidal in the category of endofunctors and applicatives are lax monoidal functors. I don't fully understand that but clearly applicatives only partially preserve the monoidal structure whereas monads fully preserve it. What are the practical implications of this difference from a perspective of a functional programmer?

I ask this question as part of the attempt to better understand applicative/monads and what causes there different expressiveness.

1
Also, the fact that Haskell's instance of Monoid for Maybe works that way has nothing to do with the theory. We could have easily defined (<>) @ Maybe to be liftA2 (<>); the fact that it isn't is more of a historical accident than anything else.Lambda Fairy
You may find the monoidal structure of monads easier to see with the monadic composition operator (<=<) :: (Monad m) => (b -> m c) -> (a -> m b) -> (a -> m c), or its flipped version (>=>), rather than the application operator (>>=). The monad laws are then identical to the monoid laws, namely left identity pure >=> x = x, right identity x >=> pure = x, and associativity (x >=> y) >=> z = x >=> (y >=> z).Jon Purdy

1 Answers

8
votes

We should sort out that we're dealing with three quite different monoids here:

  • Monoids in Haskell, i.e. instances of the Monoid class. Those are value-level monoids, i.e. you're combining together Haskell values such as lists.
  • Applicatives are, mathematically speaking, monoidal functors, but the monoid there is nothing you could write a Monoid instance for; in the Haskell context, it is the type-level monoid constructed by the unit type () and the tuple constructor (,). (Note that this is only a monoid if you pretend that e.g. (Int, (String, Bool)) and ((Int, String), Bool) are the same type.)
    Those tuples aren't directly visible in the Applicative class' methods, but that's just because they have been hidden through currying. The more generalizable formulation of the class is

    class Functor f => Monoidal f where
      funit ::    ()     -> f  ()
      fzip :: (f a, f b) -> f (a,b)
    

    It's a nice exercise to prove that this is equivalent to the standard Applicative class.
    What characterises a monoidal functor is really that it preserves this (,) monoid structure whilst doing its functor-mapping, but that doesn't really have much to do with any particular Haskell monoid.

  • Monads are, as keeps floating around as a semi-humorous buzz term, monoids in the category of endofunctors. But that's yet another monoid we're talking about, namely the monoid of functor-application stacking. Like with Applicative, the mathematical formulation of the class is a bit different:

    class Monoidal m => Monad m where
      pure ::      a  -> m a
      join :: m (m a) -> m a
    

    i.e. the monoid here is a kind of composition of applications of m.

So with that out of the way, it really shouldn't surprise us that you get different behaviour when comparing something involving monads/applicatives with some particular Haskell monoid. There are cases when you get the same behaviour with both, but that can basically be attributed to having used an instance that degenerates the higher-level monoidal structure to something that, when used with a fixed contained-type, is isomorphic to a non-parametric Monoid instance.