reducing boilerplate and combining effects: a monad transformer example

25
Reducing Boilerplate and Combining Effects: A Monad Transformer Example Scala Matsuri - Feb 25th, 2017 Connie Chen ϩα϶ЄϤϹЄϕ΄ڷ仂;አ΄奲ΕݳΥͱ: ͽ憎Ρϯϗϖ䄜䟵ৼ

Upload: connie-chen

Post on 22-Jan-2018

1.142 views

Category:

Software


0 download

TRANSCRIPT

Page 1: Reducing Boilerplate and Combining Effects: A Monad Transformer Example

Reducing Boilerplate and Combining Effects:

A Monad Transformer ExampleScala Matsuri - Feb 25th, 2017

Connie Chen

:

Page 2: Reducing Boilerplate and Combining Effects: A Monad Transformer Example

Hello

• @coni

• Data Platform team @ Twilio

• @ http://github.com/conniec

Page 3: Reducing Boilerplate and Combining Effects: A Monad Transformer Example

• Monad transformers allow different monads to compose

• Combine effects of monads to create a SUPER MONAD

• Eg. Future[Option], Future[Either], Reader[Option]

• In this example, we will use the Cats library...

What are Monad transformers?

Page 4: Reducing Boilerplate and Combining Effects: A Monad Transformer Example

Future[Either[A, B]] turns into EitherT[Future, A, B]

Future[Option[A]] turns into OptionT[Future, A]

Page 5: Reducing Boilerplate and Combining Effects: A Monad Transformer Example

import scala.concurrent.Future import cats.data.OptionT import cats.implicits._ import scala.concurrent.ExecutionContext.Implicits.global

case class Beans(fresh: Boolean = true) case class Grounds() class GroundBeansException(s: String) extends Exception(s: String)

1.

Example: Making coffee!Step 1. Grind the beans

Page 6: Reducing Boilerplate and Combining Effects: A Monad Transformer Example

def grindFreshBeans(beans: Beans, clumsy: Boolean = false): Future[Option[Grounds]] = { if (clumsy) { Future.failed(new GroundBeansException("We are bad at grinding")) } else if (beans.fresh) { Future.successful(Option(Grounds())) } else { Future.successful(None) } }

1.

Example: Making coffee!Step 1. Grind the beans

Page 7: Reducing Boilerplate and Combining Effects: A Monad Transformer Example

Step 1. Grind the beans

Three different kind of results: • Value found • Value not found • Future failed

Future 3

Example: Making coffee!

Page 8: Reducing Boilerplate and Combining Effects: A Monad Transformer Example

Step 2. Boil hot water case class Kettle(filled: Boolean = true) case class Water() case class Coffee(delicious: Boolean) class HotWaterException(s: String) extends Exception(s: String)

2.

def getHotWater(kettle: Kettle, clumsy: Boolean = false): Future[Option[Water]] = { if (clumsy) { Future.failed(new HotWaterException("Ouch spilled that water!")) } else if (kettle.filled) { Future.successful(Option(Water())) } else { Future.successful(None) } }

Page 9: Reducing Boilerplate and Combining Effects: A Monad Transformer Example

Step 3. Combine water and coffee (it's a pourover)

3. ( )

def makingCoffee(grounds: Grounds, water: Water): Future[Coffee] = { println(s"Making coffee with... $grounds and $water") Future.successful(Coffee(delicious=true)) }

Page 10: Reducing Boilerplate and Combining Effects: A Monad Transformer Example

val coffeeFut = for {

} yield Option(result)

coffeeFut.onSuccess { case Some(s) => println(s"SUCCESS: $s") case None => println("No coffee found?") }

coffeeFut.onFailure { case x => println(s"FAIL: $x") }

Without Monad transformers, success scenario

beans <- grindFreshBeans(Beans(fresh=true))

hotWater <- getHotWater(Kettle(filled=true))

beansResult = beans.getOrElse(throw new Exception("Beans result errored. ")) waterResult = hotWater.getOrElse(throw new Exception("Water result errored. "))

result <- makingCoffee(beansResult, waterResult)

Page 11: Reducing Boilerplate and Combining Effects: A Monad Transformer Example

Without Monad transformers, success scenario

coffeeFut: scala.concurrent.Future[Option[Coffee]] = scala.concurrent.impl.Promise$DefaultPromise@7404ac2

scala> Making coffee with... Grounds() and Water() SUCCESS: Coffee(true)

Page 12: Reducing Boilerplate and Combining Effects: A Monad Transformer Example

With Monad transformers, success scenario

val coffeeFutMonadT = for { beans <- OptionT(grindFreshBeans(Beans(fresh=true))) hotWater <- OptionT(getHotWater(Kettle(filled=true))) result <- OptionT.liftF(makingCoffee(beans, hotWater)) } yield result

coffeeFutMonadT.value.onSuccess { case Some(s) => println(s"SUCCESS: $s") case None => println("No coffee found?") }

coffeeFutMonadT.value.onFailure { case x => println(s"FAIL: $x") }

Page 13: Reducing Boilerplate and Combining Effects: A Monad Transformer Example

coffeeFutMonadT: cats.data.OptionT[scala.concurrent.Future,Coffee] = OptionT(scala.concurrent.impl.Promise$DefaultPromise@4a1c4b40)

scala> Making coffee with... Grounds() and Water() SUCCESS: Coffee(true)

With Monad transformers, success scenario

Page 14: Reducing Boilerplate and Combining Effects: A Monad Transformer Example

OptionT

`fromOption` gives you an OptionT from Option Internally, it is wrapping your option in a Future.successful()

`liftF` gives you an OptionT from Future Internally, it is mapping on your Future and wrapping it in a Some()

Helper functions on OptionT

Page 15: Reducing Boilerplate and Combining Effects: A Monad Transformer Example

val coffeeFut = for { beans <- grindFreshBeans(Beans(fresh=false)) hotWater <- getHotWater(Kettle(filled=true)) beansResult = beans.getOrElse(throw new Exception("Beans result errored. ")) waterResult = hotWater.getOrElse(throw new Exception("Water result errored. ")) result <- makingCoffee(beansResult, waterResult) } yield Option(result)

coffeeFut.onSuccess { case Some(s) => println(s"SUCCESS: $s") case None => println("No coffee found?") }

coffeeFut.onFailure { case x => println(s"FAIL: $x") }

Without Monad transformers, failure scenario

Page 16: Reducing Boilerplate and Combining Effects: A Monad Transformer Example

Without Monad transformers, failure scenario

coffeeFut: scala.concurrent.Future[Option[Coffee]] = scala.concurrent.impl.Promise$DefaultPromise@17ee3bd8

scala> FAIL: java.lang.Exception: Beans result errored.

Page 17: Reducing Boilerplate and Combining Effects: A Monad Transformer Example

val coffeeFutT = for { beans <- OptionT(grindFreshBeans(Beans(fresh=false))) hotWater <- OptionT(getHotWater(Kettle(filled=true))) result <- OptionT.liftF(makingCoffee(beans, hotWater)) } yield result

coffeeFutT.value.onSuccess { case Some(s) => println(s"SUCCESS: $s") case None => println("No coffee found?") }

coffeeFutT.value.onFailure { case x => println(s"FAIL: $x") }

With Monad transformers, failure scenario

Page 18: Reducing Boilerplate and Combining Effects: A Monad Transformer Example

With Monad transformers, failure scenario

coffeeFutT: cats.data.OptionT[scala.concurrent.Future,Coffee] = OptionT(scala.concurrent.impl.Promise$DefaultPromise@4e115bbc)

scala> No coffee found?

Page 19: Reducing Boilerplate and Combining Effects: A Monad Transformer Example

val coffeeFutT = for { beans <- OptionT(grindFreshBeans(Beans(fresh=true))) hotWater <- OptionT(getHotWater(Kettle(filled=true), clumsy=true)) result <- OptionT.liftF(makingCoffee(beans, hotWater)) } yield s"$result"

coffeeFutT.value.onSuccess { case Some(s) => println(s"SUCCESS: $s") case None => println("No coffee found?") }

coffeeFutT.value.onFailure { case x => println(s"FAIL: $x") }

With monad transformers, failure scenario with exception

Page 20: Reducing Boilerplate and Combining Effects: A Monad Transformer Example

FAIL: $line86.$read$$iw$$iw$$iw$$iw$$iw$$iw$$iw$$iw$HotWaterException: Ouch spilled that water!

coffeeFutT: cats.data.OptionT[scala.concurrent.Future,Coffee] = OptionT(scala.concurrent.impl.Promise$DefaultPromise@20e4013)

With monad transformers, failure scenario with exception

Page 21: Reducing Boilerplate and Combining Effects: A Monad Transformer Example

flatMap

• Use monad transformers to short circuit your monads

What did we learn?

• Instead of unwrapping layers of monads, monad transformers results in a new monad to flatMap with

• Reduce layers of x.map( y => y.map ( ... )) to just x.map ( y => ...))

x.map ( y => y.map ( ... ) ) map

Page 22: Reducing Boilerplate and Combining Effects: A Monad Transformer Example

OptionT

What’s next?

• Many other types of monad transformers: ReaderT, WriterT, EitherT, StateT

• Since monad transformers give you a monad as a result-- you can stack them too!

Page 23: Reducing Boilerplate and Combining Effects: A Monad Transformer Example

Thank you

Connie Chen - @coni Twilio

We’re hiring!

Page 24: Reducing Boilerplate and Combining Effects: A Monad Transformer Example

final case class OptionT[F[_], A](value: F[Option[A]]) {

def fold[B](default: => B)(f: A => B)(implicit F: Functor[F]): F[B] = F.map(value)(_.fold(default)(f))

def map[B](f: A => B)(implicit F: Functor[F]): OptionT[F, B] = OptionT(F.map(value)(_.map(f)))

def flatMapF[B](f: A => F[Option[B]])(implicit F: Monad[F]): OptionT[F, B] = OptionT(F.flatMap(value)(_.fold(F.pure[Option[B]](None))(f)))

OptionT implementation

Page 25: Reducing Boilerplate and Combining Effects: A Monad Transformer Example

def liftF[F[_], A](fa: F[A])(implicit F: Functor[F]): OptionT[F, A] = OptionT(F.map(fa)(Some(_)))

OptionT implementation