> Monad stacks do wonders for circumscribing your computational context in an app.
Wait until you discover the power of Applicative Functors. They are more restricted in their operations than Monads, so they compose better and allow for analysis.
Mathematically speaking applicatives are less restrictive since more things can be applicatives than monads. A monad is a "very special" kind of functor and that is why a composition of monads often fails to satisfy the monad laws. The least restricted thing in this hierarchy is the functor since composition and other kinds of operations always give you back another functor.
You can determine a lot more about a computation described only in terms of an applicative functor as you can from a monadic one, without running any of its effects (i.e. "statically").
gergoerdi covered analysis, but composition is simple. For any two Applicatives F and G and value type A the following 4 values are all Applicative values
F A
G A
F (G A)
G (F A)
But for monads only the first two are monads for general monads F and G. So, Applicatives compose better!
Even for monad transformers it's not necessarily the case that direct composition leads to a monad. For instance, ContT looks like
newtype ContT r m a = ContT ((a -> m r) -> m r)
so ContT M A is not the same as Cont (M A).
Monads also compose under products as two parallel monadic computations
data (f * g) a = Pair (f a) (g a)
p1 :: (f * g) a -> f a
p1 (Pair fa _) = fa
p2 :: (f * g) a -> g a
p2 (Pair _ ga) = ga
instance (Monad f, Monad g) => Monad (f * g) where
return a = Pair (return a) (return a)
Pair fa ga >>= k = Pair (fa >>= p1 . k) (ga >>= p2 . k)
You can also talk about composition under sums
data (f + g) a = Inl (f a) | Inr (g a)
and you'll get compositions of applicatives here so long as you have a notion of natural transformation
class Natural f g where
phi :: f a -> g a
instance (Applicative f, Applicative g, Natural f g) => Applicative (f + g) where
pure a = Inl (pure a)
Inl ff <*> Inl fa = Inl ( ff <*> fa)
Inr gf <*> Inr ga = Inr ( gf <*> ga)
Inl ff <*> Inr ga = Inr (phi ff <*> ga)
Inr gf <*> Inl fa = Inr ( gf <*> phi fa)
but there's no way to get a similar monad. It turns out that you have to "know what branch to go down before you start" in exactly the kind of way applicatives allow and monad preclude.
Wait until you discover the power of Applicative Functors. They are more restricted in their operations than Monads, so they compose better and allow for analysis.