$30 off During Our Annual Pro Sale. View Details »

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
    ՛Ԫͽ஀ᒈͺϯϗϖ䄜䟵ৼ

    View Slide

  2. ME, HI!

    View Slide

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

    View Slide

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

    View Slide

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

    View Slide

  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
    ֜ඳ: ᎣΡڹ΄ͩ;ΨΔͶ憝͞ͼ͚Ρ͡Ο

    View Slide

  7. A QUESTION

    View Slide

  8. View Slide

  9. THE PROBLEM
    val x: Future[List[Int]] = ???
    futureList.map(list => list.map(f))
    ^ ^
    |________________|
    2 maps 1 function

    View Slide

  10. CAN WE DO BETTER?
    ද࠺ͽͣ΀͚ͶΣ͜͡?

    View Slide

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

    View Slide

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

    View Slide

  13. FUNCTOR OF FUTURE
    val futureF = new Functor[Future] {
    def map[A, B](fa: Future[A])(f: A => B): Future[B] =
    fa.map(f)
    }

    View Slide

  14. future.map(f) list.map(f)
    | |
    | |
    Functor[Future].map(future)(f) Functor[List].map(list)(f)

    View Slide

  15. futureList.map(f) // not really valid scala
    | // but you get the point
    |
    Functor[Future[List]].map(futureList)(f)

    View Slide

  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))
    䋚檭΄πЄϖ

    View Slide

  17. CATS
    https://github.com/typelevel/cats

    View Slide

  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)
    ف΢ৼ΁΀͹͵ϷφϕΨϢ϶ϐϕ΁ͯΡ

    View Slide

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

    View Slide

  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 ;௏͜͠

    View Slide

  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]
    }
    ̿ϯ̀ͽতΔΡίϹ

    View Slide

  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]
    }

    View Slide

  23. A LESS CONTRIVED EXAMPLE
    case class User(name: String)
    case class Address(city: String)
    Θ͜੝ͭ䋚አጱ΀ֺ

    View Slide

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

    View Slide

  25. LET'S COMPREHEND THIS
    val getCity: Future[String] =
    for {
    gab <- getUser("Gabriele")
    address <- getAddress(gab)
    } yield address.city
    for ٖ۱ᤒ懿ͽ䨗ͧΡ

    View Slide

  26. LESSONS
    monads allow sequential execution
    monads can squash F[F[A]] into F[A]
    ϯϗϖ΅᭑ེ䋚ᤈΨݢᚆ;ͯΡ
    ϯϗϖ΅ `F[F[A]]` Ψ `F[A]` ΁ͺΌͯͩ;͢ͽͣΡ

    View Slide

  27. QUESTIONS?

    View Slide

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

    View Slide

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

    View Slide

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

    View Slide

  31. BACK TO THE REAL WORLD
    def getUser(name: String): Future[Option[User]] // better
    def getAddress(user: User): Future[Option[Address]]
    ΞΠᜉֺ͚΁΀ΠΔͭ͵

    View Slide

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

    View Slide

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

    View Slide

  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
    ;͚ͩ͜;΅...

    View Slide

  35. WHAT WE WOULD REALLY WANT
    val city: Future[Option[String]] =
    for {
    gab <- maybeUser <- getUser("Gabriele")
    address <- maybeAddress <- getAddress(gab)
    } yield address.city
    ཿͭ͡͹͵Θ΄

    View Slide

  36. futureUser.flatMap(f) maybeUser.flatMap(f)
    | |
    | |
    Monad[Future].flatMap(futureUser)(f) Monad[Option].flatMap(maybeUser)f)

    View Slide

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

    View Slide

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

    View Slide

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

    View Slide

  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)
    吖΀ΡϯϗϖΨݳ౮ͭͼϯϗϖΨ୵౮ͯΡͩ;΅ͽͣ΀͚

    View Slide

  41. MONADS DO NOT
    COMPOSE
    GENERICALLY
    ䷍አጱ΁΅ϯϗϖ΅ݳ౮ͽͣ΀͚

    View Slide

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

    View Slide

  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

    View Slide

  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]
    }

    View Slide

  45. case class FutOpt[A](value: Future[Option[A]])

    View Slide

  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
    }

    View Slide

  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
    ֵ͹ͼΕΡ

    View Slide

  48. WHAT IF
    def getUsers(query: String): List[Option[User]]
    ͩ΄䁰ݳ΅Ϳ͜ͽͭΝ͜

    View Slide

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

    View Slide

  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
    }

    View Slide

  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
    }

    View Slide

  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
    }

    View Slide

  53. A MORE GENERIC APPROACH
    case class WhateverOpt[A, W[_]](value: W[Option[A]])
    ΞΠυδϚϷϐμ΀ොဩ

    View Slide

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

    View Slide

  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

    View Slide

  56. IN GENERAL
    Foo[Bar[X]]
    becomes
    BarT[Foo, X]
    ΞΠӞᛱጱ΁΅̵Foo[Bar[X]] ΅ BarT[Foo, X] ;΀ΠΔͯ

    View Slide

  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"))
    㳨΄ֺ

    View Slide

  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"
    ͜Δ͚ͥ͡΀͚

    View Slide

  59. View Slide

  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 (ᒶϕϹ) ;ͭ͡ͼ΀͚?

    View Slide

  61. EXAMPLE: UPDATING A USER
    > check user exists
    > check it can be updated
    > update it
    ๅෛ΄ֺ

    View Slide

  62. THE NAIVE WAY
    def checkUserExists(id: String): Future[Option[User]]
    def checkCanBeUpdated(u: User): Future[Boolean]
    def updateUserOnDb(u: User): Future[User]
    ύϮ΀ොဩ

    View Slide

  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)
    }
    }
    πЄϖ΄憎᭗ͭ͢䘂͚

    View Slide

  64. DETAILED ERRORS
    from
    Option[User]
    to
    Either[MyError, User]
    托͚ͭε϶ЄΨ஑͵͚

    View Slide

  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")))
    }
    }
    抎ΕͻΟ͚ΔΔ

    View Slide

  66. !
    F[Either[A, B]]

    View Slide

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

    View Slide

  68. HOW ABOUT
    case class MyError(msg: String)
    type ResultT[F[_], A] = EitherT[F, MyError, A]
    type FutureResult[A] = ResultT[Future, A]
    ͩ΢΀ΟͿ͜ͽͭΝ͜͡

    View Slide

  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)
    }
    ϥϸϞЄ樛හΨአ఺

    View Slide

  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] = ???

    View Slide

  71. BETTER?
    def updateUser(user: User): FutureResult[User] = for {
    user <- checkUserExists(user.id)
    _ <- checkCanBeUpdated(user)
    updatedUser <- updateUser(user)
    } yield updatedUser
    Ξͥ΀͹͵ͽͭΝ͜͡

    View Slide

  72. PERSONAL TIPS
    πϑ

    View Slide

  73. TIP #1
    stacking more than two monads
    gets bad really quickly
    2ͺզӤ΄ϯϗϖΨ坌ΕӤͨΡ;ᬔͥ΀Ρ

    View Slide

  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

    View Slide

  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 ΁ڊͫ΀͚Ξ͜΁ͯΡ

    View Slide

  76. TIP #3
    !
    Perf!
    Wrapping/unwrapping isn't cheap, so if you're concerned
    about performance, consider benchmarking your code.
    ᯿͚πЄϖ΀΄ͽ௔ᚆΨ䶲΁ͯΡ΀ΟϦЀώϫЄμΨݐΡ

    View Slide

  77. TIP #4
    Use them as a ""local optimization"".
    In case your problem is not "local", consider alternative
    approaches.
    WHAT ELSE?
    ੴಅጱ΀๋晒۸΁አ͚Ρ

    View Slide

  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
    ᛔኧϯϗϖ΅ϤϺν϶ϭਧ嬝͡Ο֢አΨړ櫝ͯΡ

    View Slide

  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 ΅ϯϗϖ䄜䟵ৼ΄դ๊;΀ΡΘ΄ͽ̵֢አΨ樛හࣳጱ΁䜷͜

    View Slide

  80. CODE FOR THE EXAMPLES
    github.com/gabro/monadT

    View Slide

  81. View Slide

  82. questionsT
    @gabro27
    @buildoHQ
    @scalaitaly

    View Slide