Why are monads not closed under composition?
I think this is easiest to understand by looking at the join
operator:
join :: Monad m => m (m a) -> m a
join
is an alternative to >>=
for defining a Monad
, and is a little easier to reason about. (But now you have an exercise to do: show how to implement >>=
from join
, and how to implement join
from >>=
!)
Let's try to make a join
operation for Composed f g
and see what goes wrong. Our input is essentially a value of type f (g (f (g a)))
, and we want to produce a value of type f (g a)
. We also know that we have join
for f
and g
individually, so if we could get a value of type f (f (g (g a)))
, then we could hit it with fmap join . join
to get the f (g a)
we wanted.
Now, f (f (g (g a)))
isn't so far from f (g (f (g a)))
. All we really need is a function like this: distribute :: g (f a) -> f (g a)
. Then we could implement join
like this:
join = Compose . fmap join . join . fmap (distribute . fmap getCompose) . getCompose
Note: there are some laws that we would want distribute
to satisfy, in order to make sure that the join
we get here is lawful.
Ok, so that shows how we can compose two monads if we have a distributive law distribute :: (Monad f, Monad g) => g (f a) -> f (g a)
. Now, it could be true that every pair of monads has a distributive law. Maybe we just have to think really hard about how to write one down?
Unfortunately there are pairs of monads that don't have a distributive law. So we can answer your original question by producing two monads that definitely don't have a way of turning a g (f a)
into an f (g a)
. These two monads will witness to the fact that monads don't compose in general.
I claim that g = IO
and f = Maybe
do not have a distributive law
-- Impossible!
distribute :: IO (Maybe a) -> Maybe (IO a)
Let's think about why such a thing should be impossible. The input to this function is an IO action that goes out into the real world and eventually produces Nothing
or a Just x
. The output of this function is either Nothing
, or Just
an IO action that, when run, eventually produces x
. To produce the Maybe (IO a)
, we would have to peek into the future and predict what the IO (Maybe a)
action is going to do!
In summary:
- Monads can compose if there is a distributive law
g (f a) -> f (g a)
. (but see the addendum below) - There are some monads that don't have such a distributive law.
- Some monads can compose with each other, but not every pair of monads can compose.
Addendum: "if", but what about "only if"? If all three of F
, G
, and FG
are monads, then you can construct a natural transformation δ : ∀X. GFX -> FGX
as the composition of GFη_X : GFX -> GFGX
followed by η_{GFGX} : GFGX -> FGFGX
and then by μ_X : FGFGX -> FGX
. In Haskellese (with explicit type applications for clarity), that would be
delta :: forall f g x. (Monad f, Monad g, Monad (Compose f g))
=> g (f x) -> f (g x)
delta = join' . pure @f . fmap @g (fmap @f (pure @g))
where
-- join for (f . g), via the `Monad (Compose f g)` instance
join' :: f (g (f (g x))) -> f (g x)
join' = getCompose . join @(Compose f g) . fmap Compose . Compose
So if the composition FG
is a monad, then you can get a natural transformation with the right shape to be a distributive law. However, there are some extra constraints that fall out of making sure your distributive law satisfies the correct properties, vaguely alluded to above. As always, the n-Category Cafe has the gory details.