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