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

Monad Transformers Down to Earth

Monad Transformers Down to Earth

Avatar for Gabriele Petronella

Gabriele Petronella

March 17, 2018
Tweet

More Decks by Gabriele Petronella

Other Decks in Programming

Transcript

  1. THIS TALK: WHAT AND WHY What: The talk I wished

    I attended before banging my head against this ֜: ᝒ㴼ͯΡڹ΁抑͡΁ර͞ͼΑͭ͡͹͵ͩ;
  2. THIS TALK: WHAT AND WHY What: The talk I wished

    I attended before banging my head against this Why: Because I still remember how it was before knowing it ֜ඳ: ᎣΡڹ΄ͩ;ΨΔͶ憝͞ͼ͚Ρ͡Ο
  3. FUNCTOR OF FUTURE val futureF = new Functor[Future] { def

    map[A, B](fa: Future[A])(f: A => B): Future[B] = fa.map(f) }
  4. futureList.map(f) // not really valid scala | // but you

    get the point | Functor[Future[List]].map(futureList)(f)
  5. IN PRACTICE // create a `Functor[Future[List]]` val futureListF = Functor[Future].compose(Functor[List])

    val data: Future[List[Int]] = Future(List(1, 2, 3)) // only one map! futureListF.map(data)(_ + 1) // Future(List(2, 3, 4)) 䋚檭΄πЄϖ
  6. ABOUT FLATTENING List(1, 2, 3).map(_ + 1) // List(2, 3,

    4) List(1, 2, 3).map(n => List.fill(n)(n)) // List(List(1), List(2, 2), List(3, 3, 3)) List(1, 2, 3).map(n => List.fill(n)(n)).flatten // List(1, 2, 2, 3, 3, 3) ف΢ৼ΁΀͹͵ϷφϕΨϢ϶ϐϕ΁ͯΡ
  7. FLATMAP flatten ∘ map = flatMap so these are the

    same List(1, 2, 3).map(n => List.fill(n)(n)).flatten List(1, 2, 3).flatMap(n => List.fill(n)(n))
  8. IN OTHER WORDS when life gives you F[F[A]] you probably

    wanted flatMap e.g. val f: Future[Future[Int]] = Future(42).map(x => Future(24)) val g: Future[Int] = Future(42).flatMap(x => Future(24)) F[F[A]] Ψ憎͵Ο flatMap ;௏͜͠
  9. THE 'M' WORD trait Monad[F[_]] { def pure[A](a: A): F[A]

    def map[A, B](fa: F[A])(f: A => B): F[B] def flatMap[A, B](fa: F[A])(f: A => F[B]): F[B] } ̿ϯ̀ͽতΔΡίϹ
  10. THE 'M' WORD trait Monad[F[_]] { def pure[A](a: A): F[A]

    def map[A, B](fa: F[A])(f: A => B): F[B] def flatMap[A, B](fa: F[A])(f: A => F[B]): F[B] }
  11. A LESS CONTRIVED EXAMPLE case class User(name: String) case class

    Address(city: String) Θ͜੝ͭ䋚አጱ΀ֺ
  12. A LESS CONTRIVED EXAMPLE def getUser(name: String): Future[User] def getAddress(user:

    User): Future[Address] val getCity: Future[String] = getUser("Gabriele").flatMap( gab => getAddress(gab).map( address => address.city ) )
  13. LET'S COMPREHEND THIS val getCity: Future[String] = for { gab

    <- getUser("Gabriele") address <- getAddress(gab) } yield address.city for ٖ۱ᤒ懿ͽ䨗ͧΡ
  14. LESSONS monads allow sequential execution monads can squash F[F[A]] into

    F[A] ϯϗϖ΅᭑ེ䋚ᤈΨݢᚆ;ͯΡ ϯϗϖ΅ `F[F[A]]` Ψ `F[A]` ΁ͺΌͯͩ;͢ͽͣΡ
  15. BACK TO THE REAL WORLD def getUser(name: String): Future[User] //

    <- really? def getAddress(user: User): Future[Address] 䋚ֺͽᘍ͞ͼΕΔͭΝ͜
  16. BACK TO THE REAL WORLD def getUser(name: String): Future[Option[User]] //

    better def getAddress(user: User): Future[Option[Address]] ΞΠᜉֺ͚΁΀ΠΔͭ͵
  17. UH, OH... val city: Future[Option[String]] = for { gab <-

    getUser("Gabriele") address <- getAddress(gab) // FAIL } yield address.city ०䤂ͭͼͭΔ͚Δͯ
  18. EVENTUALLY val city: Future[Option[String]] = for { gab <- getUser("Gabriele")

    address <- getAddress(gab.get) // } yield address.get.city // get ֵ͹͵ΟύϮ
  19. OR... val city: Future[Option[String]] = for { maybeUser <- getUser("Gabriele")

    maybeCity <- maybeUser match { case Some(user) => getAddress(user).map(_.map(_.city)) case None => Future.successful(None) } } yield maybeCity ;͚ͩ͜;΅...
  20. WHAT WE WOULD REALLY WANT val city: Future[Option[String]] = for

    { gab <- maybeUser <- getUser("Gabriele") address <- maybeAddress <- getAddress(gab) } yield address.city ཿͭ͡͹͵Θ΄
  21. WHAT'S THE IMPOSSIBLE PART? // trivial def compose[F[_]: Functor, G[_]:

    Functor]: Functor[F[G[_]]] = // impossible def compose[M[_]: Monad, N[_]: Monad]: Monad[M[N[_]]] = // (not valid scala, but you get the idea) 吖΀ΡϯϗϖΨݳ౮ͭͼϯϗϖΨ୵౮ͯΡͩ;΅ͽͣ΀͚
  22. flatMap FOR Future[Option[A]] val city: Future[Option[String]] = for { maybeUser

    <- getUser("Gabriele") maybeCity <- maybeUser match { case Some(user) => getAddress(user).map(_.map(_.city)) case None => Future.successful(None) } } yield maybeCity Future[Option[String]] ΄͵Η΄ flatMap
  23. THE MONAD INTERFACE trait Monad[F[_]] { def pure[A](a: A): F[A]

    def map[A, B](fa: F[A])(f: A => B): F[B] def flatMap[A, B](fa: F[A])(f: A => F[B]): F[B] }
  24. implicit val futOptMonad: Monad[FutOpt] = new Monad[FutOpt] { def pure[A](a:

    A): FutOpt[A] = FutOpt(a.pure[Option].pure[Future]) def map[A, B](fa: FutOpt[A])(f: A => B): FutOpt[B] = FutOpt(fa.value.map(optA => optA.map(f))) def flatMap[A, B](fa: FutOpt[A])(f: A => FutOpt[B]): FutOpt[B] = FutOpt(fa.value.flatMap { case Some(a) => f(a).value case None => (None: Option[B]).pure[Future] }) // omitting tailRecM here }
  25. AND USE val f: FutOpt[String] = for { gab <-

    FutOpt(getUser("Gabriele")) address <- FutOpt(getAddress(gab)) } yield address.city // ! val city: Future[Option[String]] = f.value ֵ͹ͼΕΡ
  26. implicit val listOptMonad: Monad[ListOpt] = new Monad[ListOpt] { def pure[A](a:

    A): ListOpt[A] = ListOpt(a.pure[Option].pure[List]) def map[A, B](fa: ListOpt[A])(f: A => B): ListOpt[B] = ListOpt(fa.value.map(optA => optA.map(f))) def flatMap[A, B](fa: ListOpt[A])(f: A => ListOpt[B]): ListOpt[B] = ListOpt(fa.value.flatMap(opt => opt match { case Some(a) => f(a).value case None => (None: Option[B]).pure[List] })) // omitting tailRecM here }
  27. implicit val futOptMonad: Monad[FutOpt] = new Monad[FutOpt] { def pure[A](a:

    A): FutOpt[A] = FutOpt(a.pure[Option].pure[Future]) def map[A, B](fa: FutOpt[A])(f: A => B): FutOpt[B] = FutOpt(fa.value.map(optA => optA.map(f))) def flatMap[A, B](fa: FutOpt[A])(f: A => FutOpt[B]): FutOpt[B] = FutOpt(fa.value.flatMap { case Some(a) => f(a).value case None => (None: Option[B]).pure[Future] }) // omitting tailRecM here }
  28. implicit val futOptMonad: Monad[FutOpt] = new Monad[FutOpt] { def pure[A](a:

    => A): FutOpt[A] = FutOpt(a.pure[Option].pure[Future]) def map[A, B](fa: FutOpt[A])(f: A => B): FutOpt[B] = FutOpt(fa.value.map(optA => optA.map(f))) def flatMap[A, B](fa: FutOpt[A])(f: A => FutOpt[B]): FutOpt[B] = FutOpt(fa.value.flatMap(opt => opt match { case Some(a) => f(a).value case None => (None: Option[B]).pure[Future] })) // omitting tailRecM here }
  29. MEET OptionT val f: OptionT[Future, String] = for { gab

    <- OptionT(getUser("Gabriele")) address <- OptionT(getAddress(gab)) } yield address.city // ! val city: Future[Option[String]] = f.value
  30. ANOTHER EXAMPLE def getUser(id: String): Future[Option[User]] = ??? def getAge(user:

    User): Future[Int] = ??? def getNickname(user: User): Option[String] = ??? val lameNickname: Future[Option[String]] = ??? // e.g. Success(Some("gabro27")) 㳨΄ֺ
  31. I KNOW THE TRICK! val lameNickname: OptionT[Future, String]] = for

    { user <- OptionT(getUser("123")) age <- OptionT(getAge(user)) // sorry, nope name <- OptionT(getNickname(user)) // sorry, neither } yield s"$name$age" ͜Δ͚ͥ͡΀͚
  32. DO YOU EVEN LIFT, BRO? val lameNickname: OptionT[Future, String]] =

    for { user <- OptionT(getUser("123")) age <- OptionT.liftF(getAge(user)) name <- OptionT.fromOption(getNickname(user)) } yield s"$name$age" lift (ᒶϕϹ) ;ͭ͡ͼ΀͚?
  33. EXAMPLE: UPDATING A USER > check user exists > check

    it can be updated > update it ๅෛ΄ֺ
  34. THE NAIVE WAY def checkUserExists(id: String): Future[Option[User]] def checkCanBeUpdated(u: User):

    Future[Boolean] def updateUserOnDb(u: User): Future[User] ύϮ΀ොဩ
  35. PROBLEMS def updateUser(u: User): Future[Option[User]] = checkUserExists("foo").flatMap { maybeUser =>

    maybeUser match { case Some(user) => checkCanBeUpdated(user).flatMap { canBeUpdated => if (canBeUpdated) { updateUserOnDb(user).map(Some(_)) } else { Future.successful(None) } } case None => Future.successful(None) } } πЄϖ΄憎᭗ͭ͢䘂͚
  36. MORE PROBLEMS (DETAILED ERRORS) case class MyError(msg: String) def updateUser(u:

    User): Future[Either[MyError, User]] = checkUserExists("foo").flatMap { maybeUser => maybeUser match { case Some(user) => checkCanBeUpdated(user).flatMap { canBeUpdated => if (canBeUpdated) { updateUserOnDb(user).map(Right(_)) } else { Future.successful(Left(MyError("user cannot be updated"))) } } case None => Future.successful(Left(MyError("user does not exist"))) } } 抎ΕͻΟ͚ΔΔ
  37. HOW ABOUT case class MyError(msg: String) type ResultT[F[_], A] =

    EitherT[F, MyError, A] type FutureResult[A] = ResultT[Future, A] ͩ΢΀ΟͿ͜ͽͭΝ͜͡
  38. SOME HELPERS object FutureResult { def apply[A](a: A): FutureResult[A] =

    apply(Future.successful(a)) def apply[A](fa: Future[A]): FutureResult[A] = EitherT.liftT(fa) def apply[A](e: Either[MyError, A]): FutureResult[A] = EitherT.fromEither(e) } ϥϸϞЄ樛හΨአ఺
  39. def checkUserExists(id: String): FutureResult[User] = FutureResult { if (id ===

    "123") User("123").asRight else MyError("sorry, no user").asLeft } def checkCanBeUpdated(u: User): FutureResult[Unit] = ??? def updateUserOnDb(u: User): FutureResult[User] = ???
  40. BETTER? def updateUser(user: User): FutureResult[User] = for { user <-

    checkUserExists(user.id) _ <- checkCanBeUpdated(user) updatedUser <- updateUser(user) } yield updatedUser Ξͥ΀͹͵ͽͭΝ͜͡
  41. TIP #1 stacking more than two monads gets bad really

    quickly 2ͺզӤ΄ϯϗϖΨ坌ΕӤͨΡ;ᬔͥ΀Ρ
  42. EXAMPLE1 val effect: OptionT[EitherT[Task, String, ?], String] = for {

    first <- readName.liftM[EitherT[?[_], String, ?]].liftM[OptionT] last <- readName.liftM[(EitherT[?[_], String, ?]].liftM[OptionT] name <- if ((first.length * last.length) < 20) OptionT.some[EitherT[Task, String, ?], String](s"$first $last") else OptionT.none[EitherT[Task, String, ?], String] _ <- (if (name == "Daniel Spiewak") EitherT.fromDisjunction[Task](\/.left[String, Unit]("your kind isn't welcome here")) else EitherT.fromDisjunction[Task](\/.right[String, Unit](()))).liftM[OptionT] _ <- log(s"successfully read in $name").liftM[EitherT[?[_], String, ?]].liftM[OptionT] } yield name 1 from djspiewak/emm
  43. TIP #2 keep your transformers for youself def publicApiMethod(x: String):

    OptionT[Future, Int] = def publicApiMethod(x: String): Future[Option[Int]] = by the way val x: OptionT[Future, Int] = OptionT(Future(Option(42))) val y: Future[Option[Int]] = x.value // Future(Option(42)) ϯϗϖ䄜䟵ৼ΅ API ΁ڊͫ΀͚Ξ͜΁ͯΡ
  44. TIP #3 ! Perf! Wrapping/unwrapping isn't cheap, so if you're

    concerned about performance, consider benchmarking your code. ᯿͚πЄϖ΀΄ͽ௔ᚆΨ䶲΁ͯΡ΀ΟϦЀώϫЄμΨݐΡ
  45. TIP #4 Use them as a ""local optimization"". In case

    your problem is not "local", consider alternative approaches. WHAT ELSE? ੴಅጱ΀๋晒۸΁አ͚Ρ
  46. FREE MONADS / TAGLESS FINAL > clearly separate structure and

    interpretation > effects are separated from program definition http://typelevel.org/cats/datatypes/freemonad.html https://blog.scalac.io/exploring-tagless-final.html ᛔኧϯϗϖ΅ϤϺν϶ϭਧ嬝͡Ο֢አΨړ櫝ͯΡ
  47. EFF https://github.com/atnos-org/eff-cats "Extensible effects are an alternative to monad transformers

    for computing with effects in a functional way" based on Freer Monads, More Extensible Effects by Oleg Kiselyov Eff ΅ϯϗϖ䄜䟵ৼ΄դ๊;΀ΡΘ΄ͽ̵֢አΨ樛හࣳጱ΁䜷͜