-
Notifications
You must be signed in to change notification settings - Fork 11
Monads
flatMap is a combinator that combines mapping and flattening. flatMap takes a function that works on the nested lists and then concatenates the results back together.
map with A => M[B] flatten
M[A] --------------------------> M[M[B]] -----------> M[B]
Examples:
// Example 1
val nestedNumbers = List(List(1, 2), List(3, 4))
nestedNumbers.flatMap(x => x.map(_ * 2)) //Output: List(2, 4, 6, 8)
// Example 2
val fruits = Seq("apple", "banana", "orange")
fruits.map(_.toUpperCase) // List(APPLE, BANANA, ORANGE)
fruits.map(_.toUpperCase).flatten // List(A, P, P, L, E, B, A, N, A, N, A, O, R, A, N, G, E)
fruits.flatMap(_.toUpperCase) // List(A, P, P, L, E, B, A, N, A, N, A, O, R, A, N, G, E)
Data structures with map
and flatMap
seem to be quite common. In fact there’s a name that describes this class of a data structures together with some algebraic laws that they should have. They are called monads.
A monad M
is a parametric type M[T]
with two operations, flatMap
and unit
, that have to satisfy some laws.
trait M[T] {
def flatMap[U](f: T => M[U]): M[U]
}
def unit[T](x: T): M[T]
In the literature, flatMap
is more commonly called bind
.
Examples:
-
List
is a monad withunit(x) = List(x)
-
Set
is monad withunit(x) = Set(x)
-
Option
is a monad withunit(x) = Some(x)
-
Generator
is a monad withunit(x) = single(x)
flatMap
is an operation on each of these types, whereas unit
in Scala is different for each monad.
map
can be defined for every monad as a combination of flatMap
and unit
:
m map f == m flatMap (x => unit(f(x)))
// OR
m map f == m flatMap (f andThen unit)
To qualify as a monad, a type has to satisfy three laws:
- Associativity:
m flatMap f flatMap g == m flatMap (x => f(x) flatMap g)
- Left unit:
unit(x) flatMap f == f(x)
- Right unit:
m flatMap unit == m
Try
is similar to Option
. Where Option
can have a value of something
or none
, Try
can have a value of Success
or Failure
. It uses Java try
in its implementation.
Whereas Option
is a monad, Try
is not since the left unit law fails.
Bullet-proof principle: An expression composed from Try
, map
, flatMap
will never throw a non-fatal exception.
We have seen that for-expressions are useful not only for collections. Many other types also define map
, flatMap
, and withFilter
operations and with them for-expressions.
Examples: Generator
, Option
, Try
.
Many types that define flatMaps
are Monads. If they also define withFilter
, then they are called Monads with Zero.
The three monad laws give useful guidance in the design of library APIs
Monad is a concept or a design pattern, that simply defines a way of composing data. It is commonly used in functional programming languages. The motivation is to declutter code.
Consider tha Java code where we have 3 classes. Foo
embeds Bar
which embeds Baz
which embeds the compute
method. The bottleneck here is any of these embedded instances can be null
, so a typical way we would cope with this with multiple null checks. And note that since we are returning a null
the return type cannot be int
and has to be Integer
.
public class Foo {
public Bar getBar();
}
public class Bar {
public Baz getBaz();
}
public class Baz {
public int compute();
}
public Integer compute(Foo foo) {
if (foo != null) {
Bar bar = foo.getBar()
if (bar != null) {
Bar baz = bar.getBaz()
if (baz != null) {
return baz.compute
} else {
return null
}
} else
return null;
}
} else
return null
}
The same code can be written in Scala as below:
class Foo {
def bar : Bar
}
class Bar {
def baz : Baz
}
class Foo {
def compute : Int
}
def compute(foo: Foo): Int = {
if (foo != null) {
val bar = foo.bar
if (bar != null) {
val baz = bar.baz
if (baz != null) {
baz.compute
} else {
null
}
} else
null;
}
} else
null
}
As you can see, it is not better than the Java code. This is where we want to use Monads to declutter the code.
Hence we define a Monad
:
trait Monad[A] {
def map[B](f: A => B): Monad[B]
def flatMap[B](f: A => Monad[B]): Monad[B]
}
This trait provides a standard way of composing and sequencing operations on some contained values.
-
map
: applies a regular function to the contained values -
flatMap
: applies a "monadic" function to the contained values
Now we define the option
Monad (differs from the scala.Option
type, but works similarly)
trait Option[A]
case class Some[A](a: A) extends Option[A]
case class None[A] extends Option[A]
Now we define the map
and flatMap
to make type Option
into a Monad:
trait Option[A] {
def map[B](f: A => B): Option[B]
def flatMap[B](f: A => Option[B]): Option[B]
}
case class Some[A](a: A) extends Option[A] {
def map[B](f: A => B): Option[B] = {
Some[f(a)]
}
def flatMap[B](f: A => Option[B]): Option[B] = {
f(a)
}
}
case class None[A] extends Option[A] = {
def map[B](f: A => B): Option[B] = {
None
}
def flatMap[B](f: A => Option[B]): Option[B] = {
None
}
}
So going back to the initial cluttered example,
class Foo {
def bar : Option[Bar]
}
class Bar {
def baz : Option[Baz]
}
class Foo {
def compute : Int
}
def computeBaz(baz: Baz): Int = {
baz.compute
}
def computeBar(bar: Bar): Option[Int] = {
bar.baz.map{ computeBaz }
}
def computeFoo(foo: Foo): Option[Int] = {
foo.bar.flatMap{ computeBar }
}
def compute(maybefoo: Option[Foo]): Option[Int] = {
maybefoo.flatMap{ computeFoo }
}
This still looks very cluttered. This can be further simpliefied as:
def compute(maybefoo: Option[Foo]): Option[Int] = {
maybefoo.flatMap { foo =>
foo.bar.flatMat { bar =>
bar.baz.map { baz =>
baz.compute
}
}
}
}
This can be further simplified using a for
expression which internally uses the map
and flatMap
functions to provide syntactic sugar:
def computeAll(maybefoo: Option[Foo]): Option[Int] = {
for {
foo <- maybefoo
bar <- foo.bar
baz <- bar.baz
} yield baz.compute
}
We can also use a List
instead of an Option
above, which is by default a monad in scala.
class Foo {
def bars : List[Bar]
}
class Bar {
def bazs : List[Baz]
}
class Foo {
def compute : List[Int]
}
def computeAll(foos: List[Foo]): List[Int] = {
for {
foo <- foos
bar <- foo.bars
baz <- bar.bazs
} yield baz.compute
}
Some reading: