Yeah sure, I'm in the mood to write, but didn't want to cram too much into one post, else risk muddying the message. It turns out that doing (a -> m b) instead of (m a -> m b) can still allow really cool control flow. But first let me address some of your points.
>For the "maybe a" case, you then need to take all of your code and place it in some machinery that knows what "maybe" means, and how to extract "a" from it.
This isn't too bad, since somebody else wrote all that machinery. It's just a nice simple library that is super flexible, and integrates well with everything around it (in haskell at least).
>And then you need to write or convert all your functions to return maybe values rather than just plain values.
I was worried people might think that. The other function besides bind needed to make a monad is called "return" or "pure." It's a function of type a -> m a. For Maybe, it just changes the type from a to Maybe a and retains the value. The constructor is called Just, because it takes 1 and returns Just 1. It's like a default way to stick a pure value into this monad. You can compose this with any of your regular functions instead of writing or converting your nice pure (a -> b) functions. So if I have a pure value like 1, and want to apply a pure function f that just adds 2, but need to return a Maybe, then I can use return, as in "return (f 1)". Now it returns the maybe type. With javascript syntax lambda functions, using bind, I can write
bind(Just 1, one => {return (one + 2)})
or in haskell lambda syntax (\x -> f x) with the infix version of bind (>>=)
Just 1 >>= \one -> return (one + 2)
That way, I didn't have to convert my addition function + to return maybe values (maybePlus or something), I just used it with return.
It is even more useful with two variables:
Just 1 >>= \one ->
Just 2 >>= \two ->
aNullableFunction one >>= \x ->
return (aNonNullableFunction two) >>= \y ->
return (one * y + two * x)
Hopefully that convinces you that you don't need to convert your functions, like aNonNullableFunction, +, or * . Additionally, haskell has syntax sugar to make that chain of functions much prettier:
do
one
>All of this could be easily done with exceptions. Why do all that when something simple like an exception will do, is easily understood, and easily coded?
It's just easy to explain with simple examples that can be dealt with other ways. By no means is it restricted to simple tasks. Random number generation is a fun one. In python, if you were determined to avoid global state for random number generators, you might have all your functions take a seed, like
(x, newSeed) = randomUniform(seed)
(y, newerSeed) = randomNormal(newSeed)
return (x*y, newerSeed)
while in the haskell equivalent, you can recognize the common "takes seed -> returns value and new seed" and let all that passing around seed stuff be implicit:
do
x
It's convenient because you can program with x and y as regular integers, not fancy wrapped values, but still not have to worry about passing the random number seeds around.
It's out of the scope of a comment, but the coolest case study might be software transactional memory (STM). I'm going to do what I hate in the haskell community and link a paper that explains it better than I could, but I'm getting tired. http://research.microsoft.com/en-us/um/people/simonpj/papers... The idea is that STM let's you write code where bind appears to be simple pointer magic, but actually does things behind the scenes to keep concurrency nightmares from happening while being easy to use.
And you really don't need to grok category theory. Some of the most prolific haskellers have said they don't, which gives the rest of us hope.