Upgrade to Pro — share decks privately, control downloads, hide ads and more …

Monads in Scala

Monads in Scala

Monad as an abstract interface. Usage of Monads in Scala

Alexey Novakov

March 06, 2020
Tweet

More Decks by Alexey Novakov

Other Decks in Programming

Transcript

  1. Monads in Scala
    Alexey Novakov, Ultra Tendency, 2020

    View Slide

  2. "A monad is just a monoid in
    the category of endofunctors”

    View Slide

  3. “Monad is an abstract
    interface”
    Book: Functional Programming in Scala (Red Book)

    View Slide

  4. Option
    Either
    List
    Future
    Map
    Set
    Stream
    Vector
    Future
    Try
    And others …
    Monads in standard Scala

    View Slide

  5. flatMap
    unit
    a.k.a bind
    a.k.a pure
    What makes thing a Monad?
    “apply” in Scala

    View Slide

  6. class Box[A](v: A) {
    def flatMap[B](f: A => Box[B]): Box[B]
    = f(v)
    }
    def map[B](f: A => B): Box[B] =
    flatMap(a => new Box(f(a)))
    A Monad modeled as a class
    minimum set

    View Slide

  7. scala> new Box(1)
    res2: Box[Int] = 1
    scala> res2.map(_ + 1)
    res3: Box[Int] = 2
    scala> res3.flatMap(i => new Box(1 + i))
    res5: Box[Int] = 3

    View Slide

  8. scala> val l = List(1,2,3)
    l: List[Int] = List(1, 2, 3)
    scala> l.map(_ + 1)
    res0: List[Int] = List(2, 3, 4)
    scala> l.flatMap(i => List(i + 1))
    res1: List[Int] = List(2, 3, 4)
    List

    View Slide

  9. val isOn = Some(1)
    val isBlack = None
    def makeCoffee: Option[String] = Some(1)
    scala> isOn
    .flatMap(_ => isBlack
    .flatMap(_ => makeCoffee))
    res0: Option[String] = None
    Option
    stopped here

    View Slide

  10. trait Functor[F[_]] {
    def map[A,B](fa: F[A])(f: A => B): F[B]
    }
    trait Monad[F[_]] extends Functor[F] {
    def unit[A](a: => A): F[A]
    def flatMap[A,B](ma: F[A])(f: A => F[B]): F[B]
    def map[A,B](ma: F[A])(f: A => B): F[B] =
    flatMap(ma)(a => unit(f(a)))
    }
    Generic Monad
    another abstract interface

    View Slide

  11. Monads are like burritos

    View Slide

  12. “f” function application in flatMap & map
    depends on the concrete Monad instance
    “f” applied when:
    - Option[A]: is Some(A)
    - Either[A, B]: is Right(B)
    - List[A]: is non-empty
    - Future[A]: is ready

    View Slide

  13. Moreover,
    Monad laws are
    there

    View Slide

  14. Monad law 1. Identity
    Example
    def f(x: Int): Option[Int] = Some(x)
    scala> Some(1).flatMap(f) == f(1)
    res0: Boolean = true
    scala> f(1) == Some(1).flatMap(f)
    res1: Boolean = true

    View Slide

  15. Monad law 1.
    Left Identity
    def f[A](x: A): Monad[A] = ???
    flatMap(unit(x))(f) == f(x)
    Right Identity
    f(x) == flatMap(unit(x))(f)

    View Slide

  16. Monad law 2. Associative
    Example
    def f1(a: Int): Option[Int] = Some(a + 1)
    def f2(a: Int): Option[Int] = Some(a * 2)
    scala> Some(1).flatMap(f1).flatMap(f2)
    res0: Option[Int] = Some(4)
    scala> Some(1).flatMap(a => f1(a).flatMap(f2))
    res1: Option[Int] = Some(4)

    View Slide

  17. Monad law 2.
    Associative
    def f1[A](a: A): Monad[A]
    def f2[A](a: A): Monad[A]
    if x is a Monad instance,
    flatMap(flatMap(x)(f1))(f2) ==
    flatMap(x)(a => flatMap(f1(a))(f2))

    View Slide

  18. Functor law 1.
    Identity
    map(x)(a => a) == x
    Example: map(Some(1))(a => a) == Some(1)
    the same value returned

    View Slide

  19. Functor law 2. Associative
    Example:
    val f1 = (n: Int) => n + 1
    val f2 = (n: Int) => n * 2
    map(map(Some(1))(f1))(f2) // Some(4)
    ==
    map(Some(1))(f2 compose f1) // Some(4)

    View Slide

  20. Functor law 2.
    Associative
    map(map(x)(f1))(f2) == map(x)(f2 compose f1)
    one by one
    apply f1 then f2 in one step

    View Slide

  21. View Slide

  22. final case class Coffee(name: String)
    val isOn = Some(1)
    val coffeeName = Some("black")
    val makeCoffee = (name: String) => Some(Coffee(name))
    for {
    _ <- isOn
    name <- coffeeName
    coffee <- makeCoffee(name)
    } yield coffee
    scala> Op)on[Coffee] = Some(Coffee(black))
    Compose Option

    View Slide

  23. case class Cluster(pods: Int)
    def validateNamespace(ns: String): Either[String, Unit] = Right(())
    def clusterExists(ns: String): Either[Cluster, Unit] = Right(())
    def createCluster(ns: String, cluster: Cluster):
    Either[String, Cluster] = Right(Cluster(cluster.pods))
    val ns = "my-cluster"
    for {
    _ <- validateNamespace(ns)
    _ <- clusterExists(ns).left.map(c =>
    s"Cluster with ${c.pods} pods already exists")
    newCluster <- createCluster(ns, Cluster(4))
    } yield newCluster
    Compose Either
    scala> Either[String,Cluster] = Right(Cluster(4))

    View Slide

  24. scala> Either[String,Cluster] = LeC(
    Cluster namespace is not valid name, choose another name
    )
    def validNamespace(ns: String): Either[String, Unit] =
    if (ns == "my-cluster")
    Left(
    “Cluster namespace is not valid name, choose another name”
    ) else Right(())

    View Slide

  25. for {…} yield
    is a syntactic sugar for
    a sequence of calls:
    flatMap1(… + flatMapN(.. + map(…)))
    https://en.wikipedia.org/wiki/Powdered_sugar#/media/File:Powdered_Sugar_-_Macro.jpg

    View Slide

  26. validNamespace("my-cluster")
    .flatMap(_ =>
    clusterExists(ns)
    .left
    .map(c =>
    s"Cluster with ${c.pods} pods already exists”
    ).flatMap(_ =>
    createCluster(ns, Cluster(4))
    .map(newCluster => newCluster)
    )
    )
    Desugared

    View Slide

  27. for {
    _ <- validNamespace("my-cluster")
    _ <- clusterExists(ns)
    .left.map(c =>
    s"Cluster with ${c.pods} pods already exists")
    newCluster <- createCluster(ns, Cluster(4))
    } yield newCluster
    Sugared

    View Slide

  28. Caveat:
    different Monads do not compose.

    View Slide

  29. def validateNamespace(ns: String):
    Either[String, Unit]
    def clusterExists(ns: String):
    Option[Either[String, Cluster]]
    def createCluster(ns: String, cluster: Cluster):
    Either[String, Cluster]
    Problem
    for {
    _ <- validateNamespace(ns)
    cluster <- clusterExists(ns)
    updated <- createCluster(ns, cluster)
    } yield updated
    two monad stacks

    View Slide

  30. updated <- createCluster(ns, cluster)
    ^
    :4: error: type mismatch;
    found : Either[String,Cluster]
    required: Cluster
    cluster <- clusterExists(ns)
    ^
    :3: error: type mismatch;
    found : Op)on[Nothing]
    required: scala.u)l.Either[?,?]
    Op)on[Nothing] <: scala.u)l.Either[?,?]?
    false

    View Slide

  31. Monad Transformers

    View Slide

  32. - custom-written monad
    - specifically constructed for composition
    OptionT: Option + Any other Monad
    EitherT: Either + Any other Monad
    ReaderT: Reader + Any other Monad
    WriterT: Writer + Any other Monad
    … others

    View Slide

  33. EitherT *Example:
    unwrap second stack
    * from cats.data.EitherT.scala
    final case class EitherT[F[_], A, B](value: F[Either[A, B]]) {
    def flatMap[AA >: A, D](
    f: B => EitherT[F, AA, D])(
    implicit F: Monad[F]): EitherT[F, AA, D] =
    EitherT(F.flatMap(value) {
    case l @ Left(_) => F.pure(l.rightCast)
    case Right(b) => f(b).value
    })
    }

    View Slide

  34. case class Cluster(pods: Int, updated: Long)
    def validateNamespace(ns: String): Either[String, Unit] =
    Right(())
    def clusterExists(ns: String): Option[Either[String, Cluster]] =
    Some(Right(Cluster(3, System.currentTimeMillis())))
    def updateCluster(ns: String, cluster: Cluster):
    Either[String, Cluster] =
    Right(Cluster(cluster.pods, System.currentTimeMillis()))
    Usage

    View Slide

  35. import cats.implicits._
    import cats.data.EitherT
    val cluster = for {
    _ <- validateNamespace(ns).toEitherT[Option]
    cluster<- EitherT(clusterExists(ns))
    updated <- updateCluster(ns, cluster).toEitherT[Option]
    } yield updated
    scala> cluster.value
    Some(Right(Cluster(3,1583095558496)))
    EitherT: Either + Option
    common ground

    View Slide

  36. Error case
    def clusterExists(ns: String): Option[Either[String, Cluster]] =
    Left("Cluster is invalid").some
    scala> cluster.value
    res4: Op)on[Either[String,Cluster]] = Some(LeC(Cluster is invalid))

    View Slide

  37. Alexey Novakov
    email:
    - alexey.novakov at ultratendency.com
    - novakov.alex at gmail.com
    Blog:
    https://medium.com/se-notes-by-alexey-novakov
    https://novakov-alexey.github.io/
    Code: https://github.com/novakov-alexey
    Twitter: @alexey_novakov
    Thank you! Questions?

    View Slide