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

Monad Transformers Down to Earth

Monad Transformers Down to Earth

Gabriele Petronella

March 17, 2018
Tweet

More Decks by Gabriele Petronella

Other Decks in Programming

Transcript

  1. GABRIELE PETRONELLA MONAD TRANSFORMERS DOWN TO EARTH ՛Ԫͽ஀ᒈͺϯϗϖ䄜䟵ৼ

  2. ME, HI!

  3. STUFF I DO 樛Υ͹ͼ͚ΡϤϺυδμϕ

  4. THIS TALK: WHAT AND WHY ͩ΄ϕЄμ΄֜;֜ඳ

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

    I attended before banging my head against this ֜: ᝒ㴼ͯΡڹ΁抑͡΁ර͞ͼΑͭ͡͹͵ͩ;
  6. 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 ֜ඳ: ᎣΡڹ΄ͩ;ΨΔͶ憝͞ͼ͚Ρ͡Ο
  7. A QUESTION 㺔

  8. None
  9. THE PROBLEM val x: Future[List[Int]] = ??? futureList.map(list => list.map(f))

    ^ ^ |________________| 2 maps 1 function
  10. CAN WE DO BETTER? ද࠺ͽͣ΀͚ͶΣ͜͡?

  11. INDENT! futureList.map { list => list.map(f) } αЀϔЀϕͭͼΕΡ

  12. FUNCTOR trait Functor[F[_]] { def map[A, B](fa: F[A])(f: A =>

    B): F[B] }
  13. FUNCTOR OF FUTURE val futureF = new Functor[Future] { def

    map[A, B](fa: Future[A])(f: A => B): Future[B] = fa.map(f) }
  14. future.map(f) list.map(f) | | | | Functor[Future].map(future)(f) Functor[List].map(list)(f)

  15. futureList.map(f) // not really valid scala | // but you

    get the point | Functor[Future[List]].map(futureList)(f)
  16. 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)) 䋚檭΄πЄϖ
  17. CATS https://github.com/typelevel/cats

  18. 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) ف΢ৼ΁΀͹͵ϷφϕΨϢ϶ϐϕ΁ͯΡ
  19. 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))
  20. 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 ;௏͜͠
  21. 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] } ̿ϯ̀ͽতΔΡίϹ
  22. 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] }
  23. A LESS CONTRIVED EXAMPLE case class User(name: String) case class

    Address(city: String) Θ͜੝ͭ䋚አጱ΀ֺ
  24. 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 ) )
  25. LET'S COMPREHEND THIS val getCity: Future[String] = for { gab

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

    F[A] ϯϗϖ΅᭑ེ䋚ᤈΨݢᚆ;ͯΡ ϯϗϖ΅ `F[F[A]]` Ψ `F[A]` ΁ͺΌͯͩ;͢ͽͣΡ
  27. QUESTIONS?

  28. WAIT A SECOND... ͷΝ͹;இ͹͵

  29. WHAT ABOUT F[G[X]] F[G[X]] ΄䁰ݳ΅?

  30. BACK TO THE REAL WORLD def getUser(name: String): Future[User] //

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

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

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

    address <- getAddress(gab.get) // } yield address.get.city // get ֵ͹͵ΟύϮ
  34. 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 ;͚ͩ͜;΅...
  35. WHAT WE WOULD REALLY WANT val city: Future[Option[String]] = for

    { gab <- maybeUser <- getUser("Gabriele") address <- maybeAddress <- getAddress(gab) } yield address.city ཿͭ͡͹͵Θ΄
  36. futureUser.flatMap(f) maybeUser.flatMap(f) | | | | Monad[Future].flatMap(futureUser)(f) Monad[Option].flatMap(maybeUser)f)

  37. futureMaybeUser.flatMap(f) | | Monad[Future[Option]].flatMap(f)

  38. futureMaybeUser.flatMap(f) | | Monad[Future[Option]].flatMap(f)

  39. MONADS DO NOT COMPOSE HTTP://BLOG.TMORRIS.NET/POSTS/ MONADS-DO-NOT-COMPOSE/ ϯϗϖ΅ݳ౮ͭ΀͚

  40. 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) 吖΀ΡϯϗϖΨݳ౮ͭͼϯϗϖΨ୵౮ͯΡͩ;΅ͽͣ΀͚
  41. MONADS DO NOT COMPOSE GENERICALLY ䷍አጱ΁΅ϯϗϖ΅ݳ౮ͽͣ΀͚

  42. BUT YOU CAN COMPOSE THEM SPECIFICALLY ͵Ͷ̵ͭᇙਧ΄䁰ݳ΅ݳ౮ݢᚆ

  43. 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
  44. 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] }
  45. case class FutOpt[A](value: Future[Option[A]])

  46. 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 }
  47. 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 ֵ͹ͼΕΡ
  48. WHAT IF def getUsers(query: String): List[Option[User]] ͩ΄䁰ݳ΅Ϳ͜ͽͭΝ͜

  49. case class ListOpt[A](value: List[Option[A])

  50. 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 }
  51. 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 }
  52. 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 }
  53. A MORE GENERIC APPROACH case class WhateverOpt[A, W[_]](value: W[Option[A]]) ΞΠυδϚϷϐμ΀ොဩ

  54. MEET OptionT OptionT[F[_], A] ^ |___ any monad OptionT Ψ奧ՕͭΔͯ

  55. 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
  56. IN GENERAL Foo[Bar[X]] becomes BarT[Foo, X] ΞΠӞᛱጱ΁΅̵Foo[Bar[X]] ΅ BarT[Foo, X]

    ;΀ΠΔͯ
  57. 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")) 㳨΄ֺ
  58. 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" ͜Δ͚ͥ͡΀͚
  59. None
  60. 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 (ᒶϕϹ) ;ͭ͡ͼ΀͚?
  61. EXAMPLE: UPDATING A USER > check user exists > check

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

    Future[Boolean] def updateUserOnDb(u: User): Future[User] ύϮ΀ොဩ
  63. 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) } } πЄϖ΄憎᭗ͭ͢䘂͚
  64. DETAILED ERRORS from Option[User] to Either[MyError, User] 托͚ͭε϶ЄΨ஑͵͚

  65. 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"))) } } 抎ΕͻΟ͚ΔΔ
  66. ! F[Either[A, B]]

  67. ! EitherT[F[_], A, B]

  68. HOW ABOUT case class MyError(msg: String) type ResultT[F[_], A] =

    EitherT[F, MyError, A] type FutureResult[A] = ResultT[Future, A] ͩ΢΀ΟͿ͜ͽͭΝ͜͡
  69. 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) } ϥϸϞЄ樛හΨአ఺
  70. 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] = ???
  71. BETTER? def updateUser(user: User): FutureResult[User] = for { user <-

    checkUserExists(user.id) _ <- checkCanBeUpdated(user) updatedUser <- updateUser(user) } yield updatedUser Ξͥ΀͹͵ͽͭΝ͜͡
  72. PERSONAL TIPS πϑ

  73. TIP #1 stacking more than two monads gets bad really

    quickly 2ͺզӤ΄ϯϗϖΨ坌ΕӤͨΡ;ᬔͥ΀Ρ
  74. 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
  75. 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 ΁ڊͫ΀͚Ξ͜΁ͯΡ
  76. TIP #3 ! Perf! Wrapping/unwrapping isn't cheap, so if you're

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

    your problem is not "local", consider alternative approaches. WHAT ELSE? ੴಅጱ΀๋晒۸΁አ͚Ρ
  78. 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 ᛔኧϯϗϖ΅ϤϺν϶ϭਧ嬝͡Ο֢አΨړ櫝ͯΡ
  79. 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 ΅ϯϗϖ䄜䟵ৼ΄դ๊;΀ΡΘ΄ͽ̵֢አΨ樛හࣳጱ΁䜷͜
  80. CODE FOR THE EXAMPLES github.com/gabro/monadT

  81. None
  82. questionsT @gabro27 @buildoHQ @scalaitaly