Assume we're translating a standard do-block,
Suppose we're translating a line of the form x <- b
.
Remember that each x <- b
of a standard do-block corresponds to a different =<<
.
So each time we use =<<
, its type m a -> b
is needs to therefore address
the fact that translating x <- b
to a lambda and bind that will form
a part of a larger structure of nested lamba binds.
The type signature is polymorphic. It's saying that any type can replace a
and
any type can replace b
here. And we really mean any type! ... String, Integer, boolean, you name it!
But be careful! This polymorphism is "scoped" for this particular function application.
A nested lambda bind may have many binds,
but the a
of one bind does not necessarily have to be the same
as the a
of another bind (same for the b
s). In other words, we have an instance
of polymorphism occuring at each line of the block, not polymorphism occuring once
for the entire block.
When we say a -> M b
, it does not mean that for the do-block all the corresponding
binds have the same type input a
and the same output type M b
. What it means is that
out of all these binds, one particular bind has an input a1
and an output type of M b1
.
And another bind has type a2
and M b2
, and so on. And furthermore, we're saying that
we don't enforce any relation between a1
and a2
.
They might be same, they might be different, nobody knows!
The polymorphism occurs seperately for each bind.
Each bind is for each do-block line so to put it another way, the polymorphism
occurs on a line-by-line basis.
Key Point: At each line of a standard do-block
a -> M b
, we have polymorphism, where thex
can be any typea
and therhs
can use any typeb
to formulate a side-effect descriptionM b
. This polymorphism occurs on a line-by-line basis. So one line'sa
andb
types is not necessarily the same as another line'sa
andb
types.
If you're familar with logic, we might write this like:
for all a, for all b: a -> M b
for all a, for all b: a -> M b
The for all's are scoped such that a given bind won't necessarily share the same a and b. Each bind get's its own for all for each variable and keeps it to itself. And so polymorphism occurs at each line.
To understand this sinature, I think the best way is to pretend we are in the middle of translating a do block, and are translating a line of the
a1 ? Ma1
(x1 -> <next line translated>) =<< m1
What is the type of the ...
? Well if take the type signature: (a1 -> M b) -> M a1 -> M b
,
the overall result must be M b, and futhermore this must match the result of the lambda.
So we must have:
a1 Mb Ma1
(x1 -> ...) =<< m1
\----- Mb -------/
Furthermore, let's suppose we had another lambda inside ...
.
(x1 -> (x2 -> (...)) =<< m2 ) =<< m1
We said that this lambda bind that we just added must have type M b
-------- M b --------
(x1 -> (x2 -> (...)) =<< m2 ) =<< m1
In general, bind has type (a -> M b) -> M a -> M b
.
If we take the type signature of this newly added lambda bind: (a2 -> ?) -> M a2 -> M b
,
We see that the ? must be M b.
What we can see if that the M b propagates through.
Overall we have:
a1 Mb Ma1
(x1 -> ...) =<< m1
↑
a2 Mb Ma2
(x1 -> ((x2 -> ...) =<< m2 ) =<< m1
↑
a3 Mb Ma3
(x1 -> ((x2 -> (x3 -> ... ) =<< m3 ) =<< m2) =<< m1
↑
Mb
(x1 -> ((x2 -> (x3 -> e ) =<< m3 ) =<< m2) =<< m1
- The types a1 a2, a3, ..., an are the types of the results of side-effects.
- It's up that particular side-effect mi function to determine the type of ai
- A given monad m doesn't know what side effect functions m1, ..., mn are going to be used in advance, and hence does know what the types a1, a2, ..., an are going to be.
- Mb is the result of the final side-effect expression e nested at innermost level of the lamba-bind expression.
- The type Mb propogates throughout. The overall type of the lamba-bind expression will be Mb.
Unlike the
a
s which change at each line, theb
stays the same. That is, we don't have b1, b2, ..., bn – it's the same b throughout.
do
x1 <- m1 ← x1 :: a1 m1 :: M a1
... last-line :: M b
do
line 1: x1 <- m1
line 2: x2 <- m2 ← x2 :: a2, m2 :: M a2
...
last line: <last-line> e :: M b
do
line 1: x1 <- m1
line 2: x2 <- m2
line 3: x3 <- m3 ← x3 :: a3, m3 :: M a3
...
last line: <last-line> e :: M b
do
x1 <- m1
x2 <- m2
x3 <- m3 ← x3 :: a3, m3 :: M a3
... last-line :: M b
do
x1 <- m1
x2 <- m2
x3 <- m3
e the last line has type Mb
-
In general, for line i, we have the statement
xi <- mi
.- we are using
<-
to unwrap a constructor so:mi
has typem ai
andxi
has typeai
- line i accepts that the last line of the entire do-block will be of type
M b
despite it not even knowing what the other lines of the do-block will be. (the last lines of the do-block shown with
- we are using
-
Mb is the result of the final line of the do-block, e
-
The type Mb propogates throughout. The overall type of do-bkock Mb.
we don't have b1, b2, ..., bn – it's the same b throughout.
Key Point The
M b
propogates throughout the do-block. The types ofxi
andmi
are allowed to change at each linexi <- mi
, but what stays the same throughout isM b
. Every line of a do-block agrees that the last statement of the do-block must have typeM b
.
The polymorphism of occurs on a line-by-line basis.
by virtue of the nesting of the funcions, b
types of all the binds are the same.
What this means is that a given do block has one and only one return type M b
.
There's no need to worry about b1
, b2
, b3
, ..., bn
, it's just the same
b
throughout!
And another important result is that the M
stays the same!
What this means is that a given do-block has one, and only one
particular monad that it works with.
Previously in 1B, we complained about the mysterious placement of m
that we
see for monadic functions. There are four somewhat reasonable
possiblities to consider:
a -> b
a -> m b
← this placement of them
s was chosen, ... why?m a -> b
m a -> m b
So why did we choose this second option? After our work in 1C, we can give some intution as to why.
- Why 1. and 3. doesn't work: The overall result must be a monad. If each bind accepted functions
that had type
a -> b
in a nested lamba-bind strucrure then the eventual result would not be a monad. The same is true each bind accepted functions had typem a -> b
. So we need thatm
in the output, that is we need<something> -> m b
. - Why 4. doesn't work: Intuitively we're dealing with monads, and we like
symmetry, so surely
m a -> m b
must work in our nested lamba-bind structure! Well sadly not. We must havea -> m b
notm a -> m b
. Why is this? Well think about what this left-hand side translates to in our lamba-bind structure.
So by process of elimination we are left with 2.
- Why 2.
a -> m b
does work So we're left with only one choicea -> m b
. It means that our values in the lamba-bind are normal non-fancy values and it means that our overall result is a monad. So it's perfect! The reasons go deeper and it's very easy to give a complex mathematical reason. One reason is that this is the only type signature that obeys the Monad Laws. But unless you're a mathematician I'd recommended reading up on Monad Laws a bit later when you have more experience (see Appendix XA if you're really intersted).
m a -> a
m a -> b
← we need an extra typeb
here ... why?