reducing boilerplate and combining effects: a monad transformer example
TRANSCRIPT
Reducing Boilerplate and Combining Effects:
A Monad Transformer ExampleScala Matsuri - Feb 25th, 2017
Connie Chen
:
• 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?
Future[Either[A, B]] turns into EitherT[Future, A, B]
Future[Option[A]] turns into OptionT[Future, A]
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
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
Step 1. Grind the beans
Three different kind of results: • Value found • Value not found • Future failed
Future 3
Example: Making coffee!
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) } }
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)) }
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)
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)
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") }
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
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
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
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.
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
With Monad transformers, failure scenario
coffeeFutT: cats.data.OptionT[scala.concurrent.Future,Coffee] = OptionT(scala.concurrent.impl.Promise$DefaultPromise@4e115bbc)
scala> No coffee found?
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
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
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
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!
Thank you
Connie Chen - @coni Twilio
We’re hiring!
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
def liftF[F[_], A](fa: F[A])(implicit F: Functor[F]): OptionT[F, A] = OptionT(F.map(fa)(Some(_)))
OptionT implementation