One thing that makes it confusing is that the "popular" functions like bind
and <*>
are praxis oriented. But to understand the concepts it's easier to look at other functions first. It's also worth noting that monads stand out because they are a bit overhyped in comparison to other connected concepts.
So I will start with functors instead.
Functors offer a function (in Haskell notation) fmap :: (Functor f) => (a -> b) -> f a -> f b
. In other words you have a context f
that you can lift a function into. As you can imagine almost anything is a functor. Lists, Maybe, Either, functions, I/O, tuples, parsers... Each represents a context in which a value can appear. So you can write extremely versatile functions that work in almost any context by using fmap
or its inline variant <$>
.
What other stuff do you want to do with contexts? You might want to combine two contexts. So you might want to get a generalization of zip :: [a] -> [b] -> [(a,b)]
for example like this: pair :: (Monoidal f) => f a -> f b -> f (a,b)
.
But because it's even more useful in practice, the Haskell libraries instead offer Applicative
, which is a combination of Functor
and Monoidal
, And also of Unit
, which just adds that you can actually put values "inside" your context with unit
.
You can write extremely generic functions by just stating these three things about the context you are working in.
Monad
is just another thing you can state on top of that. What I didn't mention before is that you already have two ways to combine two contexts: You can not only pair
them, but you can also stack them, e.g. you can have a list of lists. In the I/O context, an example would be an I/O action that can read other I/O actions from a file, so you would have a type FilePath -> IO (IO a)
. How can we get rid of that stacking to get an executable function IO a
? That's where Monad
s join
comes in, it allows us to combine two stacked contexts of the same type. The same goes for parsers, Maybe etc. And bind
is just a more practical way to use join
So a monadic context only has to offer four things and it can be used with almost all the machinery developed for I/O, for parsers, for failures etc.