The monad is a pattern that naturally arises in functional everyday programming. Even if it weren’t discovered, you will be still using it every day, just without knowing its fancy (and useless) name. Here are some examples.
Suppose you’re writing a program that has some configuration data, like command-line parameters, and you want to pass it to some of your functions that depend on it. So you have a bunch of functions that has type config -> 'a
and you constantly threading this annoying config
from the caller to callee, e.g.,
let main ctxt =
let x = do_one_thing ctxt in
let y = do_another_thing x ctxt in
x + y
As soon as we see a repetitive pattern, we should look for an abstraction opportunity. In this case, our abstraction is called the reader monad and has type type 'a reader = config -> 'a
, so we can implement corresponding bind operation and write,
let main =
let* x = do_one_thing in
let+ y = do_another_thing x in
x + y
Next, suppose you’re writing code that threads the state, i.e., unlike the previous example, your functions may functionally update the passed configuration, and now instead of writing ugly code like,
let state,x = do_one_thing state in
let state,y = do_another_thing x state in
x + y
We can write a much more readable and less error-prone version, which uses the
type 'a state = config -> 'a * config
monad,
let* x = do_one_thing in
let+ y = do_another_thing x in
x + y
Now, let’s add another feature to our program. We’ve figured out that our functions can fail, so in addition to changing the state, we might also return an error. In this case, writing in the direct (non-monadic) style is even more painful and nearly impossible, e.g.,
match do_one_thing state with
| Error _ as err -> err
| Ok (state,x) -> match do_another_thing x state with
| Error _ as err -> err
| Ok (state,y) -> x + y
But in the monadic style it is still… you probably already guessed it,
let* x = do_one_thing in
let+ y = do_another_thing x in
x + y
So nothing changes, again. Notice how we were adding various effects and we didn’t need to change our business logic code. It is because we found the right abstraction. And this is the main power of monads, is that you’re able to write clear generic code and extend it without modification. This separation of concerns significantly reduces the cognition burden and you can focus on the business logic and forget about the implementation details. Thus monads enable you to tackle the more complex problems. That is especially true when you write parser-like code when you need backtracking and multiple choices.
But let’s return to our examples. You can see that the monadic version of the code is the same, the only thing that changes is the let*
operator, which stands for the bind
function (and let+
, which stands for map
, but is easily derivable from bind
). This let*
is the essence of the monad, as the monad is just an abstraction of the computation, i.e., it defines how terms are computed. And by abstracting it, or let’s say, reifying it into the let*
operator, we can write generic code that doesn’t depend on how the computation unfolds. You can also think of the built-in let
operator of OCaml as the monad, as well. It also offers error handling and state, but is less general (no backtracking, non-determinism) and, the main problem, too invasive. All computations are naturally embedded into the OCaml monad, so we can’t really say which computation is pure and which is not from its type. Unless we embrace some discipline and refrain from using the OCaml monad and stick to explicit monads.
To summarize, a monad is just an algebra of computations. We have int
that is an algebra of integers, we have Set
that is the algebra of sets. And we have the algebra of monads, which captures the idea of computation, i.e., evaluating a term and binding it to a result, i.e.,
type 'a t
val return : 'a -> 'a t
val (let*) : 'a t -> ('a -> 'b t) -> 'b t
Computations are so natural in our everyday programmer’s life, that we don’t even notice them, and don’t think about them as abstractions, thinking that it is the responsibility of the programming language abstract machine to carry computations for us. It turns out, that if we will take this power from the language and put it under our control, we can make wonders.
Algebraic effects will enable more efficient and straightforward implementations of monads, especially those that deal with non-pure effects, such as the IO monad. But by no means do they substitute the concept of the monad. In other words, the effects system is the implementation details, like algebraic data types, records, exceptions, etc. Not to be confused with the monad, which is an abstraction that can be implemented with effects, ADT, functions, etc.