Monad
Monad
extends the Applicative
type class with a
new function flatten
. Flatten takes a value in a nested context (eg.
F[F[A]]
where F is the context) and "joins" the contexts together so
that we have a single context (ie. F[A]
).
The name flatten
should remind you of the functions of the same name on many
classes in the standard library.
Option(Option(1)).flatten
// res0: Option[Int] = Some(1)
Option(None).flatten
// res1: Option[Nothing] = None
List(List(1),List(2,3)).flatten
// res2: List[Int] = List(1, 2, 3)
Monad instances
If Applicative
is already present and flatten
is well-behaved,
extending the Applicative
to a Monad
is trivial. To provide evidence
that a type belongs in the Monad
type class, cats' implementation
requires us to provide an implementation of pure
(which can be reused
from Applicative
) and flatMap
.
We can use flatten
to define flatMap
: flatMap
is just map
followed by flatten
. Conversely, flatten
is just flatMap
using
the identity function x => x
(i.e. flatMap(_)(x => x)
).
import cats._
implicit def optionMonad(implicit app: Applicative[Option]) =
new Monad[Option] {
// Define flatMap using Option's flatten method
override def flatMap[A, B](fa: Option[A])(f: A => Option[B]): Option[B] =
app.map(fa)(f).flatten
// Reuse this definition from Applicative.
override def pure[A](a: A): Option[A] = app.pure(a)
}
flatMap
flatMap
is often considered to be the core function of Monad
, and cats
follows this tradition by providing implementations of flatten
and map
derived from flatMap
and pure
.
implicit val listMonad = new Monad[List] {
def flatMap[A, B](fa: List[A])(f: A => List[B]): List[B] = fa.flatMap(f)
def pure[A](a: A): List[A] = List(a)
}
Part of the reason for this is that name flatMap
has special significance in
scala, as for-comprehensions rely on this method to chain together operations
in a monadic context.
import scala.reflect.runtime.universe
// import scala.reflect.runtime.universe
universe.reify(
for {
x <- Some(1)
y <- Some(2)
} yield x + y
).tree
// res4: reflect.runtime.universe.Tree = Some.apply(1).flatMap(((x) => Some.apply(2).map(((y) => x.$plus(y)))))
ifM
Monad
provides the ability to choose later operations in a sequence based on
the results of earlier ones. This is embodied in ifM
, which lifts an if
statement into the monadic context.
Monad[List].ifM(List(true, false, true))(List(1, 2), List(3, 4))
// res5: List[Int] = List(1, 2, 3, 4, 1, 2)
Composition
Unlike Functor
s and Applicative
s,
not all Monad
s compose. This means that even if M[_]
and N[_]
are
both Monad
s, M[N[_]]
is not guaranteed to be a Monad
.
However, many common cases do. One way of expressing this is to provide
instructions on how to compose any outer monad (F
in the following
example) with a specific inner monad (Option
in the following
example).
Note: the example below assumes usage of the kind-projector compiler plugin and will not compile if it is not being used in a project.
case class OptionT[F[_], A](value: F[Option[A]])
implicit def optionTMonad[F[_]](implicit F : Monad[F]) = {
new Monad[OptionT[F, ?]] {
def pure[A](a: A): OptionT[F, A] = OptionT(F.pure(Some(a)))
def flatMap[A, B](fa: OptionT[F, A])(f: A => OptionT[F, B]): OptionT[F, B] =
OptionT {
F.flatMap(fa.value) {
case None => F.pure(None)
case Some(a) => f(a).value
}
}
}
}
This sort of construction is called a monad transformer.
Cats has an OptionT
monad transformer, which adds a lot of useful functions to the simple implementation above.