I think the biggest problem with monad tutorials is that it seems that everyone finally "gets" monads in a different way so the monad tutorials that get written lack motivation. For example, I finally "got" monads when I was working with a promise library in Javascript...

That said, one thing I noticed is that there are two main "types" of monad tutorials. The first one is the the one taken by the OP and I call it the "category theory" approach. It tends to show what sorts of things you can do with monads and how they work mathematically but they kind of lack an explanation for why you should bother with monads in the first place.

The other approach for monads is the "monads as an useful interface" approach. One paper I would highly recommend in this category is the "awkward squad" paper by Simon Peyton Jones:

https://research.microsoft.com/en-us/um/people/simonpj/Paper...

Its an introduction to monads that helped me a lot and it also explains quite a bit about the history of monads.

Which lets me come back to your main point:

> why this is better than the alternatives (like simple function composition)

Because simple function composition doesn't work! While nowadays people like to point out that Haskell is a pure functional language, noone would have bothered with making a pure language (back then) if all you gained was all this headache with monads. The reason Haskell was pure in the first place was because its original goal was to be a lazily-evaluated language and that lazyness kind of forces you into pureness. You might think the order of evaluation of function arguments in C being unspecified is bad, but Haskell would be even worse since the order of evaluation for everything is unspecified and some things might not even end up being evaluated at all!

They ended up trying all sorts of approaches for adding effectful computation to Haskell and the most successful one was the monad one (the SPJ paper goes over this a bit). If you pay attention, the monad interface forces computations to be single threaded (you can't "branch out" monadic effects) and doesn't let you generally start or end an effectful computation wherever you want, meaning that whoever defined the monad you are using can control how effects are started or terminated via what functions they expose in the public interface (Maybe exposes the constructors directly so you can pattern match on them but IO makes it so `main` the only place you can start an IO computation)

Finally, this gets us back to notational problems. While monads make things really neat in the type level and as a platform for effects, it can end up really noisy in practice:

getNumber >>= (\a -> getNumber >>= (\b -> f a b)))

So for monads we have "do" notation that lets you write things in a neater way:

a <- getNumber
b <- getNumber
f a b

This is really helpful and 99% of the time, do notation is precisely why people bother making things into monads instead of defining their own versions of (>>=), like they do in Javascript with promises. However, writing all those intermediate variables can be annoying and doesn't feel "composeable". The Functor and Applicative type classes provide methods to do that:

f <$> getNumber <*> getNumber

Monads are always particular cases of these type classes and you can show this mathematically but its a bit complicated in practice because people added monads to Haskell before they discovered these otehr type classes were useful...

To wrap things up I would like to point out that most of the time monads and functors are an "advanced" technique that lets you take programs that you could already write before but write them in a neater way, with do notation, fmap, <$>, etc. (While its true that you can write generic programs that abstract over the monad interface, you only end up doing that if you are writing a control flow library or something even more advanced like a parser combinator). To give examples of what I am talking about, if you have functions that return null on error you can already get by with adding an if statements after every call (like you have to do in C) but monads let you omit the boilerplate if statements. A more advanced example I like is coroutines/generators (things that "yield"). You can write this sort of program in a normal language by manually inverting your control flow, storing state explicitly in stacks or other data structures. A continuation monad lets you write the code in a more natural way (and as a library, without needing to extend the base language with suport for a yield statement!)I think the biggest problem with monad tutorials is that it seems that everyone finally "gets" monads in a different way so the monad tutorials that get written lack motivation. For example, I finally "got" monads when I was working with a promise library in Javascript...

That said, one thing I noticed is that there are two main "types" of monad tutorials. The first one is the the one taken by the OP and I call it the "category theory" approach. It tends to show what sorts of things you can do with monads and how they work mathematically but they kind of lack an explanation for why you should bother with monads in the first place.

The other approach for monads is the "monads as an useful interface" approach. One paper I would highly recommend in this category is the "awkward squad" paper by Simon Peyton Jones:

https://research.microsoft.com/en-us/um/people/simonpj/Paper...

Its an introduction to monads that helped me a lot and it also explains quite a bit about the history of monads.

Which lets me come back to your main point:

> why this is better than the alternatives (like simple function composition)

Because simple function composition doesn't work! While nowadays people like to point out that Haskell is a pure functional language, noone would have bothered with making a pure language (back then) if all you gained was all this headache with monads. The reason Haskell was pure in the first place was because its original goal was to be a lazily-evaluated language and that lazyness kind of forces you into pureness. You might think the order of evaluation of function arguments in C being unspecified is bad, but Haskell would be even worse since the order of evaluation for everything is unspecified and some things might not even end up being evaluated at all!

They ended up trying all sorts of approaches for adding effectful computation to Haskell and the most successful one was the monad one (the SPJ paper goes over this a bit). If you pay attention, the monad interface forces computations to be single threaded (you can't "branch out" monadic effects) and doesn't let you generally start or end an effectful computation wherever you want, meaning that whoever defined the monad you are using can control how effects are started or terminated via what functions they expose in the public interface (Maybe exposes the constructors directly so you can pattern match on them but IO makes it so `main` the only place you can start an IO computation)

Finally, this gets us back to notational problems. While monads make things really neat in the type level and as a platform for effects, it can end up really noisy in practice:

getNumber >>= (\a -> getNumber >>= (\b -> f a b)))

So for monads we have "do" notation that lets you write things in a neater way:

a <- getNumber
b <- getNumber
f a b

This is really helpful and 99% of the time, do notation is precisely why people bother making things into monads instead of defining their own versions of (>>=), like they do in Javascript with promises. However, writing all those intermediate variables can be annoying and doesn't feel "composeable". The Functor and Applicative type classes provide methods to do that:

f <$> getNumber <*> getNumber

Monads are always particular cases of these type classes and you can show this mathematically but its a bit complicated in practice because people added monads to Haskell before they discovered these otehr type classes were useful...

To wrap things up I would like to point out that most of the time monads and functors are an "advanced" technique that lets you take programs that you could already write before but write them in a neater way, with do notation, fmap, <$>, etc. (While its true that you can write generic programs that abstract over the monad interface, you only end up doing that if you are writing a control flow library or something even more advanced like a parser combinator). To give examples of what I am talking about, if you have functions that return null on error you can already get by with adding an if statements after every call (like you have to do in C) but monads let you omit the boilerplate if statements. A more advanced example I like is coroutines/generators (things that "yield"). You can write this sort of program in a normal language by manually inverting your control flow, storing state explicitly in stacks or other data structures. A continuation monad lets you write the code in a more natural way (and as a library, without needing to extend the base language with suport for a yield statement!)