Successfully reported this slideshow.
We use your LinkedIn profile and activity data to personalize ads and to show you more relevant ads. You can change your ad preferences anytime.

Reducing Boilerplate and Combining Effects: A Monad Transformer Example

1,335 views

Published on

An example of using OptionT with Future[Option]]

Published in: Software
  • Be the first to comment

Reducing Boilerplate and Combining Effects: A Monad Transformer Example

  1. 1. Reducing Boilerplate and Combining Effects: A Monad Transformer Example Scala Matsuri - Feb 25th, 2017 Connie Chen :
  2. 2. Hello • @coni • Data Platform team @ Twilio • @ http://github.com/conniec
  3. 3. • 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?
  4. 4. Future[Either[A, B]] turns into EitherT[Future, A, B] Future[Option[A]] turns into OptionT[Future, A]
  5. 5. import scala.concurrent.Future import cats.data.OptionT import cats.implicits._ import scala.concurrent.ExecutionContext.Implicits.glo bal 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
  6. 6. 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
  7. 7. Step 1. Grind the beans Three different kind of results: • Value found • Value not found • Future failed Future 3 Example: Making coffee!
  8. 8. 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) } }
  9. 9. 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)) }
  10. 10. 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)
  11. 11. 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)
  12. 12. 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") }
  13. 13. 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
  14. 14. 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
  15. 15. 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
  16. 16. 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.
  17. 17. 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
  18. 18. With Monad transformers, failure scenario coffeeFutT: cats.data.OptionT[scala.concurrent.Future ,Coffee] = OptionT(scala.concurrent.impl.Promise $DefaultPromise@4e115bbc) scala> No coffee found?
  19. 19. 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
  20. 20. 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
  21. 21. 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
  22. 22. 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!
  23. 23. Thank you Connie Chen - @coni Twilio We’re hiring!
  24. 24. 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
  25. 25. def liftF[F[_], A](fa: F[A])(implicit F: Functor[F]): OptionT[F, A] = OptionT(F.map(fa) (Some(_))) OptionT implementation

×