Before talking about monads, we should review 3 concepts:
They appear in the form f :: a -> b
, where ::
indicates the type of f
.
This means that f receives something of type a
and gives something of type b
.
Functions are one-to-one relations: when you give f
some a
, you obtain a b
.
Those types, a
and 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 c
.
There are concrete types like Int
, Char
, Bool
, etc.
We could mention values of those types (eg. 'a' :: Char
).
There are other types like [a]
, Map a b
, Maybe a
, IO a
, etc.
These types are not concrete, they have parameters, they construct types.
We have to say who are those a
's and b
's to name posible values.
After that, we can mention the values (eg. [1, 2, 3] :: [Int]
).
Sometimes we want to have type constructors also as parameters.
If we have f :: t a -> a
, we know that a
can be anything.
But t
is a type constructor, because it has a parameter.
For some t
, t a
could be [a]
, Tree a
, Maybe a
, etc.
When we talk about some t a
, it is likely that t
has some purpose.
-
Lists
[a]
are used to store things of typea
, one after the other. -
Trees like
Tree a
are also used to store data, with other structure. -
A type
Maybe a
indicates if something of typea
is present or not. -
An
IO a
says that input/output will be done to produce ana
.
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
t
is. -
The
a
values, 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:
-
pure
andbind
are functions. -
a
andb
will be concrete types. -
m
is a type constructor (like the ones we mentioned before). -
m
surely will have its own meaning, its nature.
Now let's talk about pure :: a -> m a
.
pure
receives an a
to produce some m a
.
This means that a
is introduced into de context of m
.
If m a
is [a]
, then a
could be stored in it.
If m a
is Maybe a
, it means that we have an a
which exists.
If 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 m
.
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
.
This m a
could be obtained using pure
, as we saw previously.
It also has a function a -> m b
, to finally produce an m b
.
bind
is implemented given a deep knowledge on the nature of m
.
This means that bind
knows how to obtain an a
from the m a
.
Also, at the same time, it knows how to analize the context of m
.
What does it mean analize the context? It means take decisions.
Given the 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 b
or produce anm b
without it. -
Decide to omit certain future operations.
-
Modify the value
a
in selected situations. -
Modify the result
m b
before submit it. -
Execute custom effects.
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 a
.
As a monad it can omit areas of code where some a
is not available.
And this is done without lots of if
's or case
's.
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.
When m a
is IO a
, we have some specials powers given by the compiler.
Those are only available inside the context of m
.
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 pure
and bind
functions.
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 pure
and bind
implemented.
Try to use (or even implement) them by yourself.
Think cases where an enforced decision mechanism could help.