Gabriele Petronella
April 21, 2017
290

# Monad Transformers down to earth - Scala Days 2017 - Chicago

April 21, 2017

## Transcript

1. @gabro27 Scala Days 2017 - Chicago

2. GABRIELE PETRONELLA
TRANSFORMERS
DOWN TO EARTH

3. @gabro27 Scala Days 2017 - Chicago

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

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

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

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

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

9. A QUESTION
@gabro27 Scala Days 2017 - Chicago

10. @gabro27 Scala Days 2017 - Chicago

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

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

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

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

15. 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

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

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

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

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

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

21. 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

22. 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

23. A LESS CONTRIVED EXAMPLE
def getUser(name: String): Future[User]
val getCity: Future[String] =
getUser("Gabriele").flatMap(
)
)
@gabro27 Scala Days 2017 - Chicago

24. LET'S COMPREHEND THIS
val getCity: Future[String] =
for {
@gabro27 Scala Days 2017 - Chicago

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

26. QUESTIONS?
@gabro27 Scala Days 2017 - Chicago

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

F[G[X]]
@gabro27 Scala Days 2017 - Chicago

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

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

31. UH, OH...
val city: Future[Option[String]] =
for {
@gabro27 Scala Days 2017 - Chicago

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

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

34. futureUser.flatMap(f) maybeUser.flatMap(f)
| |
| |
@gabro27 Scala Days 2017 - Chicago

35. futureMaybeUser.flatMap(f)
|
|
@gabro27 Scala Days 2017 - Chicago

36. futureMaybeUser.flatMap(f)
|
|
@gabro27 Scala Days 2017 - Chicago

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

38. WHAT'S THE IMPOSSIBLE PART?
// trivial
def compose[F[_]: Functor, G[_]: Functor]: Functor[F[G[_]]] = ✅
// impossible
// (not valid scala, but you get the idea)
@gabro27 Scala Days 2017 - Chicago

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

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

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

42. 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

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

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

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

46. 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

47. 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

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

49. MEET OptionT
OptionT[F[_], A]
^
@gabro27 Scala Days 2017 - Chicago

50. 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

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

52. 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

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

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

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

56. 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

57. 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

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

59. 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

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

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

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

63. 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

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

65. PERSONAL TIPS
@gabro27 Scala Days 2017 - Chicago

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

67. 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
_ } yield name
1 from djspiewak/emm
@gabro27 Scala Days 2017 - Chicago

68. 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

69. TIP #3
! Perf!
Wrapping/unwrapping isn't cheap, so if you're concerned
@gabro27 Scala Days 2017 - Chicago

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

71. 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

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

> clearly separate structure and interpretation
> effects are separated from program definition
@gabro27 Scala Days 2017 - Chicago

74. 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

75. @gabro27 Scala Days 2017 - Chicago

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

77. @gabro27 Scala Days 2017 - Chicago