The intuition for FlatMap
and Monad
we built on day 5
via the tightrope walking example is that a monadic chaining >>=
can carry context from one operation to the next.
A single None
in an intermediate value banishes the entire chain.
The context monad instances carry along is different.
The State
datatype we saw on day 7 for example
automates the explicit passing of the state object with >>=
.
This is a useful intuition of monads to have in comparison to Functor
,
Apply
, and Applicative
, but it doesn’t tell the whole story.
Another intuition about monads (technically FlatMap
) is
that they are fractals, like the above Sierpinski triangle.
Each part of a fractal is self-similar to the whole shape.
Take List
for example. A List
of List
s can be treated a flat List
.
val xss = List(List(1), List(2, 3), List(4))
// xss: List[List[Int]] = List(List(1), List(2, 3), List(4))
xss.flatten
// res0: List[Int] = List(1, 2, 3, 4)
The flatten
function embodies the crunching of the List
data structure.
If we think in terms of the type signature, it would be F[F[A]] => F[A]
.
We can get a better idea of the flattening by reimplementing it using foldLeft
:
xss.foldLeft(List(): List[Int]) { _ ++ _ }
// res1: List[Int] = List(1, 2, 3, 4)
We can say that List
forms a monad under ++
.
Now let try to figure out under what operation does Option
form a monad:
val o1 = Some(None: Option[Int]): Option[Option[Int]]
// o1: Option[Option[Int]] = Some(value = None)
val o2 = Some(Some(1): Option[Int]): Option[Option[Int]]
// o2: Option[Option[Int]] = Some(value = Some(value = 1))
val o3 = None: Option[Option[Int]]
// o3: Option[Option[Int]] = None
And here’s the foldLeft
:
o1.foldLeft(None: Option[Int]) { (_, _)._2 }
// res2: Option[Int] = None
o2.foldLeft(None: Option[Int]) { (_, _)._2 }
// res3: Option[Int] = Some(value = 1)
o3.foldLeft(None: Option[Int]) { (_, _)._2 }
// res4: Option[Int] = None
It seems like Option
forms a monad under (_, _)._2
.
If we come back to the State
datatype from the point of view of fractals,
it becomes clear that a State
of State
is also a State
.
This property allows us to create mini-programs like pop
and push
,
and compose them into a larger State
using for
comprehension:
def stackManip: State[Stack, Int] = for {
_ <- push(3)
a <- pop
b <- pop
} yield(b)
We also saw similar composition with the Free
monad.
In short, monadic values compose within the same monad instance.
When you want to find your own monad, keep a lookout for the fractal structure.
From there, see if you can implement the flatten
function F[F[A]] => F[A]
.