Slide 1

Slide 1 text

GABRIELE PETRONELLA MONAD TRANSFORMERS DOWN TO EARTH ՛Ԫͽ஀ᒈͺϯϗϖ䄜䟵ৼ

Slide 2

Slide 2 text

ME, HI!

Slide 3

Slide 3 text

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

Slide 4

Slide 4 text

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

Slide 5

Slide 5 text

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

Slide 6

Slide 6 text

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

Slide 7

Slide 7 text

A QUESTION 㺔

Slide 8

Slide 8 text

No content

Slide 9

Slide 9 text

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

Slide 10

Slide 10 text

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

Slide 11

Slide 11 text

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

Slide 12

Slide 12 text

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

Slide 13

Slide 13 text

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

Slide 14

Slide 14 text

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

Slide 15

Slide 15 text

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

Slide 16

Slide 16 text

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

Slide 17

Slide 17 text

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

Slide 18

Slide 18 text

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

Slide 19

Slide 19 text

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

Slide 20

Slide 20 text

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

Slide 21

Slide 21 text

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

Slide 22

Slide 22 text

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

Slide 23

Slide 23 text

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

Slide 24

Slide 24 text

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

Slide 25

Slide 25 text

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

Slide 26

Slide 26 text

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

Slide 27

Slide 27 text

QUESTIONS?

Slide 28

Slide 28 text

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

Slide 29

Slide 29 text

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

Slide 30

Slide 30 text

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

Slide 31

Slide 31 text

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

Slide 32

Slide 32 text

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

Slide 33

Slide 33 text

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

Slide 34

Slide 34 text

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

Slide 35

Slide 35 text

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

Slide 36

Slide 36 text

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

Slide 37

Slide 37 text

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

Slide 38

Slide 38 text

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

Slide 39

Slide 39 text

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

Slide 40

Slide 40 text

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

Slide 41

Slide 41 text

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

Slide 42

Slide 42 text

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

Slide 43

Slide 43 text

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

Slide 44

Slide 44 text

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

Slide 45

Slide 45 text

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

Slide 46

Slide 46 text

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 }

Slide 47

Slide 47 text

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

Slide 48

Slide 48 text

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

Slide 49

Slide 49 text

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

Slide 50

Slide 50 text

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 }

Slide 51

Slide 51 text

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 }

Slide 52

Slide 52 text

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 }

Slide 53

Slide 53 text

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

Slide 54

Slide 54 text

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

Slide 55

Slide 55 text

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

Slide 56

Slide 56 text

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

Slide 57

Slide 57 text

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

Slide 58

Slide 58 text

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

Slide 59

Slide 59 text

No content

Slide 60

Slide 60 text

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

Slide 61

Slide 61 text

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

Slide 62

Slide 62 text

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

Slide 63

Slide 63 text

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

Slide 64

Slide 64 text

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

Slide 65

Slide 65 text

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

Slide 66

Slide 66 text

! F[Either[A, B]]

Slide 67

Slide 67 text

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

Slide 68

Slide 68 text

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

Slide 69

Slide 69 text

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

Slide 70

Slide 70 text

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

Slide 71

Slide 71 text

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

Slide 72

Slide 72 text

PERSONAL TIPS πϑ

Slide 73

Slide 73 text

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

Slide 74

Slide 74 text

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

Slide 75

Slide 75 text

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

Slide 76

Slide 76 text

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

Slide 77

Slide 77 text

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

Slide 78

Slide 78 text

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

Slide 79

Slide 79 text

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

Slide 80

Slide 80 text

CODE FOR THE EXAMPLES github.com/gabro/monadT

Slide 81

Slide 81 text

No content

Slide 82

Slide 82 text

questionsT @gabro27 @buildoHQ @scalaitaly