Gabriele Petronella
March 17, 2018
2.1k

# Monad Transformers Down to Earth

March 17, 2018

## Transcript

1. GABRIELE PETRONELLA
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. THE PROBLEM
val x: Future[List[Int]] = ???
futureList.map(list => list.map(f))
^ ^
|________________|
2 maps 1 function

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

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

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

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

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

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

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

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

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

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

19. 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]] Ψ憎͵Ο ﬂatMap ;௏͜͠

20. THE 'M' WORD
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]
}
̿ϯ̀ͽতΔΡίϹ

21. THE 'M' WORD
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. A LESS CONTRIVED EXAMPLE
case class User(name: String)
Θ͜੝ͭ䋚አጱ΀ֺ

23. A LESS CONTRIVED EXAMPLE
def getUser(name: String): Future[User]
val getCity: Future[String] =
getUser("Gabriele").flatMap(
)
)

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

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

26. QUESTIONS?

27. WAIT A
SECOND...
ͷΝ͹;இ͹͵

F[G[X]]
F[G[X]] ΄䁰ݳ΅?

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

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

31. UH, OH...
val city: Future[Option[String]] =
for {
gab <- getUser("Gabriele")
०䤂ͭͼͭΔ͚Δͯ

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

33. OR...
val city: Future[Option[String]] = for {
maybeUser <- getUser("Gabriele")
maybeCity <- maybeUser match {
case None => Future.successful(None)
}
} yield maybeCity
;͚ͩ͜;΅...

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

35. futureUser.flatMap(f) maybeUser.flatMap(f)
| |
| |

36. futureMaybeUser.flatMap(f)
|
|

37. futureMaybeUser.flatMap(f)
|
|

COMPOSE
HTTP://BLOG.TMORRIS.NET/POSTS/
ϯϗϖ΅ݳ౮ͭ΀͚

39. WHAT'S THE IMPOSSIBLE PART?
// trivial
def compose[F[_]: Functor, G[_]: Functor]: Functor[F[G[_]]] =
// impossible
// (not valid scala, but you get the idea)
吖΀ΡϯϗϖΨݳ౮ͭͼϯϗϖΨ୵౮ͯΡͩ;΅ͽͣ΀͚

COMPOSE
GENERICALLY
䷍አጱ΁΅ϯϗϖ΅ݳ౮ͽͣ΀͚

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

42. flatMap FOR Future[Option[A]]
val city: Future[Option[String]] = for {
maybeUser <- getUser("Gabriele")
maybeCity <- maybeUser match {
case None => Future.successful(None)
}
} yield maybeCity
Future[Option[String]] ΄͵Η΄ ﬂatMap

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

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

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
}

46. AND USE
val f: FutOpt[String] =
for {
gab <- FutOpt(getUser("Gabriele"))
!
val city: Future[Option[String]] = f.value
ֵ͹ͼΕΡ

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

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

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
}

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
}

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
}

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

53. MEET OptionT
OptionT[F[_], A]
^
OptionT Ψ奧ՕͭΔͯ

54. MEET OptionT
val f: OptionT[Future, String] =
for {
gab <- OptionT(getUser("Gabriele"))
!
val city: Future[Option[String]] = f.value

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

70. PERSONAL TIPS
πϑ

71. TIP #1
2ͺզӤ΄ϯϗϖΨ坌ΕӤͨΡ;ᬔͥ΀Ρ

72. EXAMPLE1
val effect: OptionT[EitherT[Task, String, ?], String] = for {
name <- if ((first.length * last.length) < 20)
else
_ <- (if (name == "Daniel Spiewak")
else
_ <- log(s"successfully read in \$name").liftM[EitherT[?[_], String, ?]].liftM[OptionT]
} yield name
1 from djspiewak/emm

73. TIP #2
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 ΁ڊͫ΀͚Ξ͜΁ͯΡ

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

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

76. FREE MONADS / TAGLESS FINAL
> clearly separate structure and interpretation
> effects are separated from program definition
https://blog.scalac.io/exploring-tagless-final.html
ᛔኧϯϗϖ΅ϤϺν϶ϭਧ嬝͡Ο֢አΨړ櫝ͯΡ

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

78. CODE FOR THE EXAMPLES