Oh, THAT'S how a monad is a monoid!
October 9, 2021
I’ve been coding in Haskell for about 5 years now and I feel very comfortable with monads, but I still balk at the oft-quoted definition1:
“a monad is a monoid in the category of endofunctors, what’s the problem?”
It was only today, reading this blog post, that something clicked for me. Now, there are many many many many many many posts and articles on this, so I’m not even going to try to provide a comprehensive, robust explanation. However, I do want to point out some key insights that made things click, in hopes that it helps some other poor soul out there.
The biggest thing that got me was how the monadic interface implements the monoidal interface. Once I got this piece, everything else fit into place. See, my problem was trying to figure out how
return :: a -> m a
(>>=) :: m a -> (a -> m b) -> m b
represented the mempty
and mappend
operations, respectively. Sure, I can squint my eyes and kind of see how return
is akin to the identity function and >>=
does some combining of monadic actions, but it wasn’t very convincing to me. Turns out, that’s the wrong way to think about it! The actual monoidal definition of monads uses join
, not >>=
!
(Of course, you can implement each in terms of the other, so return
/join
is just as descriptive of a monad as return
/>>=
)
Concretely, this is how I get the Monoid
interface and the Monad
interface to look analogous in my head:
class Monoid a where
mempty :: () -> a
mappend :: (a, a) -> a
class Monad m where
return :: Identity a -> m a
join :: m (m a) -> m a
With some hand-waving, these are equivalent definitions to the standard definitions in Haskell:
mempty
now takes in unit as an argument, which is redundant but equivalentmappend
is uncurriedreturn
now takesIdentity a
instead ofa
, which would be annoying in practice to wrap and unwrap, but sinceIdentity a
is effectively an alias fora
, they’re equivalent at the type level- And as discussed before,
join
is included in theMonad
interface instead of>>=
, but they’re equal in power (join mm = mm >>= id
;m >>= f = join (fmap f m)
)
After being convinced that these definitions are equivalent to the current definitions, you can now squint and see the resemblence:
mempty :: () -> a
return :: Identity _ -> m _
mappend :: (a, a) -> a
join :: m (m _) -> m _
After making this connection, everything else fell into place pretty easily. I’m not going to go any further for now (because there are already plenty of other resources out there), but hopefully this gives someone out there the last push of insight the way it did for me.