Reducing Boilerplate and Combining
Effects:
A Monad Transformer Example
Scala Matsuri - Feb 25th, 2017
Connie Chen
:
Hello
• @coni
• Data Platform team @ Twilio
• @ http://github.com/conniec
• 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.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
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

Reducing Boilerplate and Combining Effects: A Monad Transformer Example

  • 1.
    Reducing Boilerplate andCombining Effects: A Monad Transformer Example Scala Matsuri - Feb 25th, 2017 Connie Chen :
  • 2.
    Hello • @coni • DataPlatform team @ Twilio • @ http://github.com/conniec
  • 3.
    • Monad transformersallow 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.
    Future[Either[A, B]] turnsinto EitherT[Future, A, B] Future[Option[A]] turns into OptionT[Future, A]
  • 5.
    import scala.concurrent.Future import cats.data.OptionT importcats.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.
    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.
    Step 1. Grindthe beans Three different kind of results: • Value found • Value not found • Future failed Future 3 Example: Making coffee!
  • 8.
    Step 2. Boilhot 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.
    Step 3. Combinewater 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.
    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.
    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.
    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.
    coffeeFutMonadT: cats.data.OptionT[scala.concurrent.Future, Coffee] = OptionT(scala.concurrent.impl.Promise $DefaultPromise@4a1c4b40) scala> Makingcoffee with... Grounds() and Water() SUCCESS: Coffee(true) With Monad transformers, success scenario
  • 14.
    OptionT `fromOption` gives youan 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.
    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.
    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.
    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.
    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.
    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.
    FAIL: $line86.$read$$iw$$iw$$iw$$iw$$iw$$iw$$iw$$iw $HotWaterException: Ouchspilled 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.
    flatMap • Use monadtransformers 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.
    OptionT What’s next? • Manyother types of monad transformers: ReaderT, WriterT, EitherT, StateT • Since monad transformers give you a monad as a result-- you can stack them too!
  • 23.
    Thank you Connie Chen- @coni Twilio We’re hiring!
  • 24.
    final case classOptionT[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.
    def liftF[F[_], A](fa:F[A])(implicit F: Functor[F]): OptionT[F, A] = OptionT(F.map(fa) (Some(_))) OptionT implementation