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. Option Either List Future Map Set Stream Vector Future Try

    And others … Monads in standard Scala
  2. 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
  3. 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
  4. 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
  5. 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
  6. 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
  7. “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
  8. 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
  9. 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)
  10. 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)
  11. 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))
  12. Functor law 1. Identity map(x)(a => a) == x Example:

    map(Some(1))(a => a) == Some(1) the same value returned
  13. 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)
  14. 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
  15. 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))
  16. 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(())
  17. 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
  18. 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
  19. 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
  20. 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
  21. 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
  22. - 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
  23. 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 }) }
  24. 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
  25. 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
  26. 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))
  27. 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?