Before talking about monads, we should review 3 concepts:
They appear in the form
f :: a -> b, where
:: indicates the type of
This means that f receives something of type
a and gives something of type
Functions are one-to-one relations: when you give
a, you obtain a
b, could be any types, even functions.
When you see
g :: a -> b -> c, that really means
g :: a -> (b -> c).
If you see
h :: (a -> b) -> c, it means that
h consumes a function and produces a
Concrete types and Type constructors
There are concrete types like
We could mention values of those types (eg.
'a' :: Char).
There are other types like
Map a b,
IO a, etc.
These types are not concrete, they have parameters, they construct types.
We have to say who are those
b's to name posible values.
After that, we can mention the values (eg.
[1, 2, 3] :: [Int]).
Higher order types
Sometimes we want to have type constructors also as parameters.
If we have
f :: t a -> a, we know that
a can be anything.
t is a type constructor, because it has a parameter.
t a could be
Maybe a, etc.
The nature of a higher order type
When we talk about some
t a, it is likely that
t has some purpose.
[a]are used to store things of type
a, one after the other.
Tree aare also used to store data, with other structure.
Maybe aindicates if something of type
ais present or not.
IO asays that input/output will be done to produce an
We could mention even more examples, but I'm sure you got the idea.
This means for
t a that:
There is a context whose nature depends on what
avalues, if any, will be found in that context.
We will work inside
t context, and take advantage of it.
We could think about Monads as some kind of programming design pattern.
Haskell let's us encode patterns, and enforce them with the compiler.
First we should know that a monad
m defines at least two functions:
pure :: a -> m a bind :: m a -> (a -> m b) -> m b
Let me break down them for you, first we can talk about the types:
bwill be concrete types.
mis a type constructor (like the ones we mentioned before).
msurely will have its own meaning, its nature.
Now let's talk about
pure :: a -> m a.
pure receives an
a to produce some
This means that
a is introduced into de context of
m a is
a could be stored in it.
m a is
Maybe a, it means that we have an
a which exists.
m a is
IO a, we already have an
a, then i/o operation is no needed.
As you can see,
pure is a way to give
a a meaning with respect to
And what about
bind :: m a -> (a -> m b) -> m b?
bind is the operation which gives power to the Monad, let's see why.
It receives a
m a, which is a value
a in the context of
m a could be obtained using
pure, as we saw previously.
It also has a function
a -> m b, to finally produce an
bind is implemented given a deep knowledge on the nature of
This means that
bind knows how to obtain an
a from the
Also, at the same time, it knows how to analize the context of
What does it mean analize the context? It means take decisions.
a and the context around it, the next step could be decided.
This is not a minor detail, using this power we could:
Execute the function
a -> m bor produce an
m bwithout it.
Decide to omit certain future operations.
Modify the value
ain selected situations.
Modify the result
m bbefore submit it.
Execute custom effects.
What does this mean for the types we saw before?
In the case of
[a], you can take decisions while inspecting or creating a list.
This means that you could omit or modify the list based on its own elements.
It is used to provide comprehensions, a powerful tool for working with lists.
This type represents the notion of the presence of some
As a monad it can omit areas of code where some
a is not available.
And this is done without lots of
This is possible because
bind itself encodes all this bookkeeping.
Think about the Maybe monad before writing nested conditionals.
It also gives a way to avoid null checks in other languages.
m a is
IO a, we have some specials powers given by the compiler.
Those are only available inside the context of
We can access it via the
bind function, which knowns the context.
bind enforces various properties, so
IO could not be used outside.
In this way,
IO is separated of your pure functions.
But they can coexist thanks to the
What to do next?
Find places where you are using (or could use) type constructors.
See if you can find repeated patterns in all that code.
Try to understand the inner nature of those types.
Search if they have
Try to use (or even implement) them by yourself.
Think cases where an enforced decision mechanism could help.