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

Monad Transformers down to earth - Scala Days 2017 - Chicago

Monad Transformers down to earth - Scala Days 2017 - Chicago

Gabriele Petronella

April 21, 2017
Tweet

More Decks by Gabriele Petronella

Other Decks in Programming

Transcript

  1. @gabro27 Scala Days 2017 - Chicago

    View Slide

  2. GABRIELE PETRONELLA
    MONAD
    TRANSFORMERS
    DOWN TO EARTH

    View Slide

  3. @gabro27 Scala Days 2017 - Chicago

    View Slide

  4. ME, HI!
    @gabro27 Scala Days 2017 - Chicago

    View Slide

  5. STUFF I DO
    @gabro27 Scala Days 2017 - Chicago

    View Slide

  6. THIS TALK:
    WHAT AND WHY
    @gabro27 Scala Days 2017 - Chicago

    View Slide

  7. THIS TALK: WHAT AND WHY
    What: The talk I wished I attended before banging my head
    against this
    @gabro27 Scala Days 2017 - Chicago

    View Slide

  8. 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
    @gabro27 Scala Days 2017 - Chicago

    View Slide

  9. A QUESTION
    @gabro27 Scala Days 2017 - Chicago

    View Slide

  10. @gabro27 Scala Days 2017 - Chicago

    View Slide

  11. THE PROBLEM
    val x: Future[List[Int]] = ???
    futureList.map(list => list.map(f))
    ^ ^
    |________________|
    2 maps 1 function
    @gabro27 Scala Days 2017 - Chicago

    View Slide

  12. CAN WE DO BETTER?
    @gabro27 Scala Days 2017 - Chicago

    View Slide

  13. INDENT!
    futureList.map { list =>
    list.map(f)
    }
    @gabro27 Scala Days 2017 - Chicago

    View Slide

  14. View Slide

  15. FUNCTOR
    trait Functor[F[_]] {
    def map[A, B](fa: F[A])(f: A => B): F[B]
    }
    @gabro27 Scala Days 2017 - Chicago

    View Slide

  16. FUNCTOR OF FUTURE
    val futureF = new Functor[Future] {
    def map[A, B](fa: Future[A])(f: A => B): Future[B] =
    fa.map(f)
    }
    @gabro27 Scala Days 2017 - Chicago

    View Slide

  17. future.map(f) list.map(f)
    | |
    | |
    Functor[Future].map(future)(f) Functor[List].map(list)(f)
    @gabro27 Scala Days 2017 - Chicago

    View Slide

  18. futureList.map(f) // not really valid scala
    | // but you get the point
    |
    Functor[Future[List]].map(futureList)(f)
    @gabro27 Scala Days 2017 - Chicago

    View Slide

  19. IN PRACTICE
    import scala.concurrent.Future
    import scala.concurrent.ExecutionContext.Implicits.global
    import cats._; import std.future._; import std.list._
    // 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))
    @gabro27 Scala Days 2017 - Chicago

    View Slide

  20. CATS
    https://github.com/typelevel/cats
    @gabro27 Scala Days 2017 - Chicago

    View Slide

  21. 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)
    @gabro27 Scala Days 2017 - Chicago

    View Slide

  22. FLATMAP
    flatten ∘ map = flatMap
    so
    List(1, 2, 3).map(n => List.fill(n)(n)).flatten
    ==
    List(1, 2, 3).flatMap(n => List.fill(n)(n))
    @gabro27 Scala Days 2017 - Chicago

    View Slide

  23. 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))
    @gabro27 Scala Days 2017 - Chicago

    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
    )
    )
    @gabro27 Scala Days 2017 - Chicago

    View Slide

  25. LET'S COMPREHEND THIS
    val getCity: Future[String] =
    for {
    gab address } yield address.city
    @gabro27 Scala Days 2017 - Chicago

    View Slide

  26. LESSONS
    monads allow sequential execution
    monads can squash F[F[A]] into F[A]
    @gabro27 Scala Days 2017 - Chicago

    View Slide

  27. QUESTIONS?
    @gabro27 Scala Days 2017 - Chicago

    View Slide

  28. WAIT A
    SECOND...
    @gabro27 Scala Days 2017 - Chicago

    View Slide

  29. WHAT ABOUT
    F[G[X]]
    @gabro27 Scala Days 2017 - Chicago

    View Slide

  30. BACK TO THE REAL WORLD
    def getUser(name: String): Future[User] // def getAddress(user: User): Future[Address]
    @gabro27 Scala Days 2017 - Chicago

    View Slide

  31. BACK TO THE REAL WORLD
    def getUser(name: String): Future[Option[User]] // better
    def getAddress(user: User): Future[Option[Address]]
    @gabro27 Scala Days 2017 - Chicago

    View Slide

  32. UH, OH...
    val city: Future[Option[String]] =
    for {
    gab address } yield address.city
    @gabro27 Scala Days 2017 - Chicago

    View Slide

  33. EVENTUALLY
    val city: Future[Option[String]] =
    for {
    gab address } yield address.get.city // !
    @gabro27 Scala Days 2017 - Chicago

    View Slide

  34. View Slide

  35. DO YOU EVEN YIELD, BRO?
    val city: Future[Option[String]] =
    for {
    maybeGab } yield for {
    gab } yield for {
    maybeAddress } yield for {
    address } yield address.city
    @gabro27 Scala Days 2017 - Chicago

    View Slide

  36. futureUser.flatMap(f) maybeUser.flatMap(f)
    | |
    | |
    Monad[Future].flatMap(futureUser)(f) Monad[Option].flatMap(maybeUser)f)
    @gabro27 Scala Days 2017 - Chicago

    View Slide

  37. futureMaybeUser.flatMap(f)
    |
    |
    Monad[Future[Option]].flatMap(f)
    @gabro27 Scala Days 2017 - Chicago

    View Slide

  38. futureMaybeUser.flatMap(f)
    |
    |
    Monad[Future[Option]].flatMap(f)
    @gabro27 Scala Days 2017 - Chicago

    View Slide

  39. MONADS DO NOT
    COMPOSE
    HTTP://BLOG.TMORRIS.NET/POSTS/
    MONADS-DO-NOT-COMPOSE/
    @gabro27 Scala Days 2017 - Chicago

    View Slide

  40. View Slide

  41. 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)
    @gabro27 Scala Days 2017 - Chicago

    View Slide

  42. MONADS DO NOT
    COMPOSE
    GENERICALLY
    @gabro27 Scala Days 2017 - Chicago

    View Slide

  43. BUT YOU CAN
    COMPOSE THEM
    SPECIFICALLY
    @gabro27 Scala Days 2017 - Chicago

    View Slide

  44. case class FutOpt[A](value: Future[Option[A])
    @gabro27 Scala Days 2017 - Chicago

    View Slide

  45. 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]
    }))
    }
    @gabro27 Scala Days 2017 - Chicago

    View Slide

  46. AND USE
    val f: FutOpt[String] =
    for {
    gab address } yield address.city // !
    val city: Future[Option[String]] = f.value
    @gabro27 Scala Days 2017 - Chicago

    View Slide

  47. WHAT IF
    def getUsers(query: String): List[Option[User]]
    @gabro27 Scala Days 2017 - Chicago

    View Slide

  48. case class ListOpt[A](value: List[Option[A])
    @gabro27 Scala Days 2017 - Chicago

    View Slide

  49. 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]
    }))
    }
    @gabro27 Scala Days 2017 - Chicago

    View Slide

  50. 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]
    }))
    }
    @gabro27 Scala Days 2017 - Chicago

    View Slide

  51. A MORE GENERIC APPROACH
    case class WhateverOpt[A, W[_]](value: W[Option[A]])
    @gabro27 Scala Days 2017 - Chicago

    View Slide

  52. MEET OptionT
    OptionT[F[_], A]
    ^
    |___ any monad
    @gabro27 Scala Days 2017 - Chicago

    View Slide

  53. MEET OptionT
    val f: OptionT[Future, String] =
    for {
    gab address } yield address.city // !
    val city: Future[Option[String]] = f.value
    @gabro27 Scala Days 2017 - Chicago

    View Slide

  54. IN GENERAL
    Foo[Bar[X]]
    becomes
    BarT[Foo, X]
    @gabro27 Scala Days 2017 - Chicago

    View Slide

  55. 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"))
    @gabro27 Scala Days 2017 - Chicago

    View Slide

  56. I KNOW THE TRICK!
    val lameNickname: OptionT[Future, String]] =
    for {
    user age name } yield s"$name$age"
    @gabro27 Scala Days 2017 - Chicago

    View Slide

  57. View Slide

  58. DO YOU EVEN LIFT, BRO?
    val lameNickname: OptionT[Future, String]] =
    for {
    user age name } yield s"$name$age"
    @gabro27 Scala Days 2017 - Chicago

    View Slide

  59. EXAMPLE: UPDATING A USER
    > check user exists
    > check it can be updated
    > update it
    @gabro27 Scala Days 2017 - Chicago

    View Slide

  60. THE NAIVE WAY
    def checkUserExists(id: String): Future[Option[User]]
    def checkCanBeUpdated(u: User): Future[Boolean]
    def updateUserOnDb(u: User): Future[User]
    @gabro27 Scala Days 2017 - Chicago

    View Slide

  61. PROBLEMS
    def updateUser(u: User): Future[Option[User]] =
    checkUserExists.flatMap { maybeUser =>
    maybeUser match {
    case Some(user) => checkCanBeUpdated(user).flatMap { canBeUpdated =>
    if (canBeUpdated) {
    updateUserOnDb(u)
    } else {
    Future(None)
    }
    }
    case None => Future(None)
    }
    }
    @gabro27 Scala Days 2017 - Chicago

    View Slide

  62. DETAILED ERRORS
    from
    Option[User]
    to
    Either[MyError, User]
    @gabro27 Scala Days 2017 - Chicago

    View Slide

  63. MORE PROBLEMS (DETAILED ERRORS)
    case class MyError(msg: String)
    def updateUser(user: User): Future[Either[MyError, User]] =
    checkUserExists(user.id).flatMap { maybeUser =>
    maybeUser match {
    case Some(user) =>
    checkCanBeUpdated(user).flatMap { canBeUpdated =>
    if (canBeUpdated) {
    updateUserOnDb(u).map(_.right)
    } else {
    Future(MyError("user cannot be updated").left)
    }
    }
    case None => Future(MyError("user not existing").left)
    }
    }
    }
    @gabro27 Scala Days 2017 - Chicago

    View Slide

  64. !
    F[Either[A, B]]
    @gabro27 Scala Days 2017 - Chicago

    View Slide

  65. !
    EitherT[F[_], A, B]
    @gabro27 Scala Days 2017 - Chicago

    View Slide

  66. HOW ABOUT
    case class MyError(msg: String)
    type ResultT[F[_], A] = EitherT[F, MyError, A]]
    type FutureResult[A] = ResultT[Future, A]
    @gabro27 Scala Days 2017 - Chicago

    View Slide

  67. BETTER?
    def checkUserExists(id: String): FutureResult[User] = Future {
    if (id === "123")
    User("123").right
    else
    MyError("sorry, no user").left
    }
    def checkCanBeUpdated(u: User): FutureResult[User] = ???
    def updateUserOnDb(u: User): FutureResult[User] = ???
    @gabro27 Scala Days 2017 - Chicago

    View Slide

  68. BETTER?
    def updateUser(user: User): FutureResult[User] = for {
    user _ updatedUser } yield updatedUser
    @gabro27 Scala Days 2017 - Chicago

    View Slide

  69. PERSONAL TIPS
    @gabro27 Scala Days 2017 - Chicago

    View Slide

  70. TIP #1
    stacking more than two monads
    gets bad really quickly
    @gabro27 Scala Days 2017 - Chicago

    View Slide

  71. EXAMPLE1
    val effect: OptionT[EitherT[Task, String, ?], String] = for {
    first last name OptionT.some[EitherT[Task, String, ?], String](s"$first $last")
    else
    OptionT.none[EitherT[Task, String, ?], String]
    _ EitherT.fromDisjunction[Task](\/.left[String, Unit]("your kind isn't welcome here"))
    else
    EitherT.fromDisjunction[Task](\/.right[String, Unit](()))).liftM[OptionT]
    _ } yield name
    1 from djspiewak/emm
    @gabro27 Scala Days 2017 - Chicago

    View Slide

  72. 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))
    @gabro27 Scala Days 2017 - Chicago

    View Slide

  73. TIP #3
    ! Perf!
    Wrapping/unwrapping isn't cheap, so if you're concerned
    about performance, consider benchmarking your code.
    @gabro27 Scala Days 2017 - Chicago

    View Slide

  74. TIP #4
    Use them as a ""local optimization"".
    In case your problem is not "local", consider alternative
    approaches.
    @gabro27 Scala Days 2017 - Chicago

    View Slide

  75. MONAD TRANSFORMERS: TAKEAWAYS
    > they end with T
    > F[G[X]] becomes GT[F[_], X]
    > can be stacked undefinitely, but gets awkward
    > they are a tool for working with stacked monads
    @gabro27 Scala Days 2017 - Chicago

    View Slide

  76. WHAT ELSE?
    @gabro27 Scala Days 2017 - Chicago

    View Slide

  77. FREE MONADS
    > clearly separate structure and interpretation
    > effects are separated from program definition
    http://typelevel.org/cats/datatypes/freemonad.html
    @gabro27 Scala Days 2017 - Chicago

    View Slide

  78. 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
    @gabro27 Scala Days 2017 - Chicago

    View Slide

  79. View Slide

  80. @gabro27 Scala Days 2017 - Chicago

    View Slide

  81. questionsT
    @gabro27
    @buildoHQ
    @scalaitaly
    @gabro27 Scala Days 2017 - Chicago

    View Slide

  82. @gabro27 Scala Days 2017 - Chicago

    View Slide