Monads
A non-mathematical introduction to Monads.
Read Functor first!
Code working with databases, performing HTTP requests, publishing messages on queues, all return something like IO[A]
in functional codebases.
Here is a real-world example, slightly modified to protect the guilty, making calls to remote services and queues.
def completeSubscription(token: String, id: UserId): F[Unit] =
for {
now <- Clock[F].instant // F[Instant]
token <- createToken(tokenString, now) // F[Token]
_ <- activate(now, token, id) now) // F[Unit]
_ <- entitleUser(id, token) // F[Unit]
_ <- sendSignupEmail(id, token) // F[Unit]
} yield ()
Remember that a for-comprehension
in Scala is just syntactic sugar for map
and flatMap
.
Here is a simpler example:
def foo(x: F[Int], y: F[Int]): F[Int] =
for {
xv <- x
yv <- y
} yield xv + yv
which desugars to:
def sum(x: F[Int], y: F[Int]): F[Int] =
x.flatMap(xv => y.map(yv => xv + yv))
sum
and completeSubscription
do not care about F
itself, the so-called effect, they care about the values contained by it.
The F[A]
returned from each of the generators in the for-comprehension
must provide flatMap
and map
, it doesn’t need anything else.
Monads
, which are also Functors
, are values that provide map
and flatMap
for an instance of an effect, F[_]
.
Here is sum
rewritten using a Monad
for any F[_]
that has a Monad[F]
:
def sum[F[_]](x: F[Int], y: F[Int])(using Monad[F]): F[Int] =
for {
xv <- x
yv <- y
} yield xv + yv
Writing a Monad
A Monad
is a Functor
and also provides the function described above for some F[_]
:
trait Monad[F[_]] extends Functor[F] {
extension [A, B](x: F[A])
def flatMap(f: A => F[B]): F[B]
}
Here are example instances for Option
and List
:
given Monad[Option] with {
extension [A, B](m: Option[A])
override def map(f: A => B): Option[B] = m.map(f)
override def flatMap(f: A => Option[B]): Option[B] = m.flatMap(f)
}
given Monad[List] with {
extension [A, B](m: List[A])
override def map(f: A => B): List[B] = m.map(f)
override def map(f: A => List[B]): List[B] = m.flatMap(f)
}
Remember that Scala’s List
and Option
have flatMap
functions already,
but don’t confuse that with the Monad implementations.
Your own types can play too:
final case class Blub[A](v: A)
object Blub {
given Monad[Blub] with {
extension [A, B](blub: Blub[A])
override def flatMap(f: A => Blub[B]): Blub[B] = f(blub.v)
}
}
sum(Blub(1), Blub(2)) // Blub(3)
More
- A Monad is also a Functor
- Monad has three laws:
- left identity:
(Monad[F].pure(x).flatMap(f)) === f(x)
- right identity:
(m.flatMap(Monad[F].pure(_))) === m
- associativity:
(m.flatMap(f)).flatMap(g) === m.flatMap(x => f(x).flatMap(g))
- left identity:
- Learn about this in pictures (Haskell)
- What we talk about when we talk about monads.pdf