What are Monads?
When you read about which programming languages you can learn to understand some language concepts, Haskell is often stated as represantant for a pure functional language. Often the next thing you can read is that Haskell has the unique concept of Monads, usually with no further explanation. I’ll try to explain in a simple way what Monads are about and how they are used in Haskell to keep the language pure.
A simplified explanation of Monads
The use case for Monads is that you have a sequence of operations and you want to keep some context that should not be lost between the operations. A few examples what a context can be and their corresponding Monads: the information that a previous step was not successful (Maybe or Either Monads), handling multiple possibilities (List Monad), tagging an operation that has side-effects (IO Monad) or logging information about previous steps (Writer Monad).
The Monad type class
As very first step, we’ll look at how Monads are definied in Haskell. Actually they are a type class (in other languages you’d call it interface) with following definition
So in the type class we have to functions that need to be implemented for every Monad (return
and >>=
) and two functions which have a default implementation (>>
and fail
).
What is the purpose of these functions?
- The
return
function is used to wrap a normal value into the Monad. - The
>>=
takes a Monad value and a function that returns a Monad value from the wrapped value - The
>>
allows to chain two Monadic values. In its default implemenation, it just ignores the first parameter and keeps the second - The
fail
function wraps an error message into a Monad. In its default implementation it ends the program with an error.
The Maybe Monad as example
One of the most simple Monads is the Maybe
Monad. Maybe a
is a class like Optional<T>
in Java that can either have a value (e.g. if a=Int: Just 10
) or no value (Nothing
). The purpose of this Monad is to chain some calculations and keep track if any of these calculations was Nothing
. In that case everything which follows will be Nothing
as well.
That means that the functions are defined like this:
Let’s look at some example: We’ll define a function that keep tracks of the number of items are available in a store. Everytime something is bought, we substract the number. When no items are left, we have Nothing
and it always stays Nothing
. We define a function takeFromStore
:
But wait: we need Int -> Maybe Int
as signature to apply it with >>=
!
You’re right and we’ll use the fact that we have currying in Haskell, so that we can partially apply function e.g. (takeFromStore 10) and use them as parameter for >>=
.
E.g. let’s consider the following sequence:
This will return Just 17
. If we continue and apply takeFromStore 18
, we’ll get Nothing
as result and from then on, we’ll always get Nothing
.
Actually, the main purpose of the Maybe Monad is to keep the state to Nothing
once it is in this stage. This is a good way to mark a sequence of operations as failed.
do blocks
As alternative notation to chains of >>=
and >>
, you can also use do
blocks to perform Monad actions. A sequence of takeFromStore
can also be written in a function like this:
This is equivalent to Just 500 >>= takeFromStore 234 >>= takeFromStore 122 >>= takeFromStore 300 >>= takeFromStore 5
. The disadvantage is that you have to bind every intermediate result to a variable. It looks pretty much like code in an imperative language.
Now comes the thing that makes the do notation a bit counter-intuitive sometimes:
When you call takeFromStoreButReturnOldValue 400
, you’ll get Just 166
as result, as you would expect. When you call takeFromStoreButReturnOldValue 300
you may expect to get Just 66
, but instead it returns Nothing
.
What has happened? There are two reasons why we do not get the expected result here. The first one is that return
does not work like you know it from languages like Java or C. It does not return a value, but just wraps it into a Monad. The second unusual thing is that the result of the function is not return after1Step
but Just initialCount >>= takeFromStore 234 >>= takeFromStore 122 >> return after1Step
. So we apply the >>
function with return after1Step
as second parameter. As the first parameter is the result of the previous line (so takeFromStore 122 after1Step
which equals Nothing
) the result of the do
block is
For me this is the most crucial thing to understand about do
blocks: they may look like some imperative way to write Haskell programs, but every line that doesn’t start with let
has to be a Monad action and is either applied to >>
or >>=
functions, so that the context (in the case of Maybe: did we have a Nothing value somewhere) is passed from step to stop. So even if the result of a step happening before is returned, we don’t lose the “side effects” that happen in steps that happened later.
The Writer Monad
A very good example for a Monad is the Writer Monad with two type parameters, Writer a b
where type a
has to be a Monoid (a type that implements the operations mappend
and mempty
for example a list). For example we can define the following operation to log numbers:
We can now use it in the following code:
This will return WriterT (Identity (500,["Got number: 3","Got number: 5","Got number: 10"]))
. Here as well you can see that even if we only have return 500
(which equals WriterT (Identity (500,[]))
we still get the logs from the previous operations as the context is preserved. So the Writer Monad can be used to manage an event log for example.
The IO Monad
Finally we’ll finish with the IO Monad. The IO Monad can be seen as a context showing that we are performing unpure operations having side effects. You can think of the context being the sequence of side-effects that has occured. Here is an example of how to use the IO Monad:
We have two functions here that perform IO actions. The first one is main
that will be executed when you run the compile program and the second one is getName
that reads a name from standard input. The getName function could also have been written like this without do
block:
getName = putStr "What's your name? " >> hFlush stdout >> getLine
What is special about the IO Monad is that unlike most other Monads there is no function IO a -> a
to extract the value from the IO Monad (expect the <-
operator that can only be used inside a do block). So you cannat get rid of the IO Monad and therefor cannot use any IO actions inside your function that return something of type Int for example. This is Haskell’s way of separating unpure IO actions from pure side-effect free code.
Conclusion
Monads are not so tough to reason about once you stop thinking that they are complicated. They are just a wrapper that keeps a context attached to a value.