Gabriele Petronella
June 23, 2017
130

# Monad Transformers down to earth - Scala Swarm 2017 - Porto

June 23, 2017

## Transcript

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

I attended before banging my head against this @gabro27 Scala Swarm 2017
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 @gabro27 Scala Swarm 2017

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

^ ^ |________________| 2 maps 1 function @gabro27 Scala Swarm 2017

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

B): F[B] } @gabro27 Scala Swarm 2017
13. ### 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 Swarm 2017

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

get the point | Functor[Future[List]].map(futureList)(f) @gabro27 Scala Swarm 2017
16. ### 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 Swarm 2017
17. ### 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 Swarm 2017

19. ### 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 Swarm 2017
20. ### 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 Swarm 2017
21. ### 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 Swarm 2017
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] } @gabro27 Scala Swarm 2017
23. ### 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] } @gabro27 Scala Swarm 2017

26. ### LESSONS monads allow sequential execution monads can squash F[F[A]] into

F[A] @gabro27 Scala Swarm 2017

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 @gabro27 Scala Swarm 2017

Swarm 2017

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

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 @gabro27 Scala Swarm 2017
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] } @gabro27 Scala Swarm 2017

46. ### 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 Swarm 2017
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 @gabro27 Scala Swarm 2017

50. ### 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 Swarm 2017
51. ### 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 Swarm 2017
52. ### 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 Swarm 2017
53. ### A MORE GENERIC APPROACH case class WhateverOpt[A, W[_]](value: W[Option[A]]) @gabro27

Scala Swarm 2017

Swarm 2017
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 @gabro27 Scala Swarm 2017

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")) @gabro27 Scala Swarm 2017
58. ### I KNOW THE TRICK! val lameNickname: OptionT[Future, String]] = for

{ user <- OptionT(getUser("123")) age <- OptionT(getAge(user)) // sorry, nope name <- OptionT(getName(user)) // sorry, neither } yield s"\$name\$age" @gabro27 Scala Swarm 2017
59. ### DO YOU EVEN LIFT, BRO? val lameNickname: OptionT[Future, String]] =

for { user <- OptionT(getUser("123")) age <- OptionT.liftF(getAge(user)) name <- OptionT.fromOption(getName(user)) } yield s"\$name\$age" @gabro27 Scala Swarm 2017
60. ### EXAMPLE: UPDATING A USER > check user exists > check

it can be updated > update it @gabro27 Scala Swarm 2017
61. ### THE NAIVE WAY def checkUserExists(id: String): Future[Option[User]] def checkCanBeUpdated(u: User):

Future[Boolean] def updateUserOnDb(u: User): Future[User] @gabro27 Scala Swarm 2017
62. ### 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 Swarm 2017

2017
64. ### 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 Swarm 2017

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

EitherT[F, MyError, A] type FutureResult[A] = ResultT[Future, A] @gabro27 Scala Swarm 2017
68. ### 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) } @gabro27 Scala Swarm 2017
69. ### 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] = ??? @gabro27 Scala Swarm 2017
70. ### BETTER? def updateUser(user: User): FutureResult[User] = for { user <-

checkUserExists(user.id) _ <- checkCanBeUpdated(user) updatedUser <- updateUser(user) } yield updatedUser @gabro27 Scala Swarm 2017

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

quickly @gabro27 Scala Swarm 2017
73. ### 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 @gabro27 Scala Swarm 2017
74. ### 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 Swarm 2017

76. ### TIP #4 Use them as a ""local optimization"". In case

your problem is not "local", consider alternative approaches. @gabro27 Scala Swarm 2017
77. ### 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 Swarm 2017

79. ### FREE MONADS > clearly separate structure and interpretation > effects

are separated from program definition http://typelevel.org/cats/datatypes/freemonad.html @gabro27 Scala Swarm 2017
80. ### 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 Swarm 2017