I really don't seem to be understanding Map and FlatMap. What I am failing to understand is how a for-comprehension is a sequence of nested calls to map and flatMap. The following example is from Functional Programming in Scala
def bothMatch(pat:String,pat2:String,s:String):Option[Boolean] = for {
f <- mkMatcher(pat)
g <- mkMatcher(pat2)
} yield f(s) && g(s)
translates to
def bothMatch(pat:String,pat2:String,s:String):Option[Boolean] =
mkMatcher(pat) flatMap (f =>
mkMatcher(pat2) map (g => f(s) && g(s)))
The mkMatcher method is defined as follows:
def mkMatcher(pat:String):Option[String => Boolean] =
pattern(pat) map (p => (s:String) => p.matcher(s).matches)
And the pattern method is as follows:
import java.util.regex._
def pattern(s:String):Option[Pattern] =
try {
Some(Pattern.compile(s))
}catch{
case e: PatternSyntaxException => None
}
It will be great if someone could shed some light on the rationale behind using map and flatMap here.
I'm not a scala mega mind so feel free to correct me, but this is how I explain the
flatMap/map/for-comprehension
saga to myself!To understand
for comprehension
and it's translation toscala's map / flatMap
we must take small steps and understand the composing parts -map
andflatMap
. But isn'tscala's flatMap
justmap
withflatten
you ask thyself! if so why do so many developers find it so hard to get the grasp of it or offor-comprehension / flatMap / map
. Well, if you just look at scala'smap
andflatMap
signature you see they return the same return typeM[B]
and they work on the same input argumentA
(at least the first part to the function they take) if that's so what makes a difference?Our plan
map
.flatMap
.for comprehension
.`Scala's map
scala map signature:
But there is a big part missing when we look at this signature, and it's - where does this
A
comes from? our container is of typeA
so its important to look at this function in the context of the container -M[A]
. Our container could be aList
of items of typeA
and ourmap
function takes a function which transform each items of typeA
to typeB
, then it returns a container of typeB
(orM[B]
)Let's write map's signature taking into account the container:
Note an extremely highly highly important fact about map - it bundles automatically in the output container
M[B]
you have no control over it. Let's us stress it again:map
chooses the output container for us and its going to be the same container as the source we work on so forM[A]
container we get the sameM
container only forB
M[B]
and nothing else!map
does this containerization for us we just give a mapping fromA
toB
and it would put it in the box ofM[B]
will put it in the box for us!You see you did not specify how to
containerize
the item you just specified how to transform the internal items. And as we have the same containerM
for bothM[A]
andM[B]
this meansM[B]
is the same container, meaning if you haveList[A]
then you are going to have aList[B]
and more importantlymap
is doing it for you!Now that we have dealt with
map
let's move on toflatMap
.Scala's flatMap
Let's see its signature:
You see the big difference from map to
flatMap
in flatMap we are providing it with the function that does not just convert fromA to B
but also containerizes it intoM[B]
.why do we care who does the containerization?
So why do we so much care of the input function to map/flatMap does the containerization into
M[B]
or the map itself does the containerization for us?You see in the context of
for comprehension
what's happening is multiple transformations on the item provided in thefor
so we are giving the next worker in our assembly line the ability to determine the packaging. imagine we have an assembly line each worker does something to the product and only the last worker is packaging it in a container! welcome toflatMap
this is it's purpose, inmap
each worker when finished working on the item also packages it so you get containers over containers.The mighty for comprehension
Now let's looks into your for comprehension taking into account what we said above:
What have we got here:
mkMatcher
returns acontainer
the container contains a function:String => Boolean
<-
they translate toflatMap
except for the last one.f <- mkMatcher(pat)
is first insequence
(thinkassembly line
) all we want out of it is to takef
and pass it to the next worker in the assembly line, we let the next worker in our assembly line (the next function) the ability to determine what would be the packaging back of our item this is why the last function ismap
.The last
g <- mkMatcher(pat2)
will usemap
this is because its last in assembly line! so it can just do the final operation withmap( g =>
which yes! pulls outg
and uses thef
which has already been pulled out from the container by theflatMap
therefore we end up with first:mkMatcher(pat) flatMap (f // pull out f function give item to next assembly line worker (you see it has access to
f
, and do not package it back i mean let the map determine the packaging let the next assembly line worker determine the container. mkMatcher(pat2) map (g => f(s) ...)) // as this is the last function in the assembly line we are going to use map and pull g out of the container and to the packaging back, itsmap
and this packaging will throttle all the way up and be our package or our container, yah!This can be traslated as:
Run this for a better view of how its expanded
results are:
This is similar to
flatMap
- loop through each element inpat
and foreach elementmap
it to each element inpat2
TL;DR go directly to the final example
I'll try and recap
Definitions
The
for
comprehension is a syntax shortcut to combineflatMap
andmap
in a way that's easy to read and reason about.Let's simplify things a bit and assume that every
class
that provides both aforementioned methods can be called amonad
and we'll use the symbolM[A]
to mean amonad
with an inner typeA
.Examples
Some commonly seen monads
List[String]
whereM[_]: List[_]
A: String
Option[Int]
whereM[_]: Option[_]
A: Int
Future[String => Boolean]
whereM[_]: Future[_]
A: String => Boolean
map and flatMap
Defined in a generic monad
M[A]
e.g.
for expression
Each line in the expression using the
<-
symbol is translated to aflatMap
call, except for the last line which is translated to a concludingmap
call, where the "bound symbol" on the left-hand side is passed as the parameter to the argument function (what we previously calledf: A => M[B]
):A for-expression with only one
<-
is converted to amap
call with the expression passed as argument:Now to the point
As you can see, the
map
operation preserves the "shape" of the originalmonad
, so the same happens for theyield
expression: aList
remains aList
with the content transformed by the operation in theyield
.On the other hand each binding line in the
for
is just a composition of successivemonads
, which must be "flattened" to maintain a single "external shape".Suppose for a moment that each internal binding was translated to a
map
call, but the right-hand was the sameA => M[B]
function, you would end up with aM[M[B]]
for each line in the comprehension.The intent of the whole
for
syntax is to easily "flatten" the concatenation of successive monadic operations (i.e. operations that "lift" a value in a "monadic shape":A => M[B]
), with the addition of a finalmap
operation that possibly performs a concluding transformation.I hope this explains the logic behind the choice of translation, which is applied in a mechanical way, that is:
n
flatMap
nested calls concluded by a singlemap
call.A contrived illustrative example
Meant to show the expressiveness of the
for
syntaxCan you guess the type of
valuesList
?As already said, the shape of the
monad
is maintained through the comprehension, so we start with aList
incompany.branches
, and must end with aList
.The inner type instead changes and is determined by the
yield
expression: which iscustomer.value: Int
valueList
should be aList[Int]
First,
mkMatcher
returns a function whose signature isString => Boolean
, that's a regular java procedure which just runPattern.compile(string)
, as shown in thepattern
function. Then, look at this lineThe
map
function is applied to the result ofpattern
, which isOption[Pattern]
, so thep
inp => xxx
is just the pattern you compiled. So, given a patternp
, a new function is constructed, which takes a Strings
, and check ifs
matches the pattern.Note, the
p
variable is bounded to the compiled pattern. Now, it's clear that how a function with signatureString => Boolean
is constructed bymkMatcher
.Next, let's checkout the
bothMatch
function, which is based onmkMatcher
. To show howbothMathch
works, we first look at this part:Since we got a function with signature
String => Boolean
frommkMatcher
, which isg
in this context,g(s)
is equivalent toPattern.compile(pat2).macher(s).matches
, which returns if the String s matches patternpat2
. So how aboutf(s)
, it's same asg(s)
, the only difference is that, the first call ofmkMatcher
usesflatMap
, instead ofmap
, Why? BecausemkMatcher(pat2) map (g => ....)
returnsOption[Boolean]
, you will get a nested resultOption[Option[Boolean]]
if you usemap
for both call, that's not what you want .The rationale is to chain monadic operations which provides as a benefit, proper "fail fast" error handling.
It is actually pretty simple. The
mkMatcher
method returns anOption
(which is a Monad). The result ofmkMatcher
, the monadic operation, is either aNone
or aSome(x)
.Applying the
map
orflatMap
function to aNone
always returns aNone
- the function passed as a parameter tomap
andflatMap
is not evaluated.Hence in your example, if
mkMatcher(pat)
returns a None, the flatMap applied to it will return aNone
(the second monadic operationmkMatcher(pat2)
will not be executed) and the finalmap
will again return aNone
. In other words, if any of the operations in the for comprehension, returns a None, you have a fail fast behavior and the rest of the operations are not executed.This is the monadic style of error handling. The imperative style uses exceptions, which are basically jumps (to a catch clause)
A final note: the
patterns
function is a typical way of "translating" an imperative style error handling (try
...catch
) to a monadic style error handling usingOption