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

7a04b88e1469561db6da3818348d4b8f?s=128

Alexey Novakov

March 06, 2020
Tweet

Transcript

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

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

    endofunctors”
  3. “Monad is an abstract interface” Book: Functional Programming in Scala

    (Red Book)
  4. Option Either List Future Map Set Stream Vector Future Try

    And others … Monads in standard Scala
  5. flatMap unit a.k.a bind a.k.a pure What makes thing a

    Monad? “apply” in Scala
  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
  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
  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
  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
  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
  11. Monads are like burritos

  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
  13. Moreover, Monad laws are there

  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
  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)
  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)
  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))
  18. Functor law 1. Identity map(x)(a => a) == x Example:

    map(Some(1))(a => a) == Some(1) the same value returned
  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)
  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
  21. None
  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
  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))
  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(())
  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
  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
  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
  28. Caveat: different Monads do not compose.

  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
  30. updated <- createCluster(ns, cluster) ^ <pas)e>:4: error: type mismatch; found

    : Either[String,Cluster] required: Cluster cluster <- clusterExists(ns) ^ <pas)e>:3: error: type mismatch; found : Op)on[Nothing] required: scala.u)l.Either[?,?] Op)on[Nothing] <: scala.u)l.Either[?,?]? false
  31. Monad Transformers

  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
  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 }) }
  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
  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
  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))
  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?