Classy Monad Transformers (Stop Eff’ing)
Sukant Hajra / @shajra
March 24, 2017
Sukant Hajra / @shajra Classy Monad Transformers (Stop Eff’ing) March 24, 2017 1 / 46
Goals
introduce monad transformers
illustrate ergonomics in Scala
recommend a usage
begin comparison with alternatives
Sukant Hajra / @shajra Classy Monad Transformers (Stop Eff’ing) March 24, 2017 2 / 46
Quick mention
Figure 1: My Employer
Sukant Hajra / @shajra Classy Monad Transformers (Stop Eff’ing) March 24, 2017 3 / 46
Materials
This presentation and all code is
at github.com/shajra/shajra-presentations/tree/master/scala-mtl
compiler-checked by Rob Norris’s sbt-tut plugin.
Sukant Hajra / @shajra Classy Monad Transformers (Stop Eff’ing) March 24, 2017 4 / 46
In lieu of time
Assuming knowledge of
Scala implicits
type classes
for-yield sugar w.r.t. Monad.
Sukant Hajra / @shajra Classy Monad Transformers (Stop Eff’ing) March 24, 2017 5 / 46
Monads, Explicitly
1trait Monad[M[_]] {
3def pure[A](a: A): M[A]
5def flatMap[A, B](ma: M[A])(f: A => M[B]): M[B] =
6flatten(map(ma)(f))
8def flatten[A](mma: M[M[A]]): M[A] =
9flatMap(mma)(identity)
11def map[A, B](ma: M[A])(f: A => B): M[B] =
12flatMap(ma)(f andThen pure)
14}
Note: the Monad type class has three laws
very important, but elided for time
Sukant Hajra / @shajra Classy Monad Transformers (Stop Eff’ing) March 24, 2017 6 / 46
Monad syntax with implicits
For convenience (e.g. with for-yield)
1implicit class OpsA[A](a: A) {
3def pure[M[_]](implicit M: Monad[M]): M[A] =
4M pure a
6}
8implicit class
9MonadOps[M[_], A](ma: M[A])(implicit M: Monad[M]) {
11def map[B](f: A => B): M[B] =
12M.map(ma)(f)
14def flatMap[B](f: A => M[B]): M[B] =
15M.flatMap(ma)(f)
17}
Sukant Hajra / @shajra Classy Monad Transformers (Stop Eff’ing) March 24, 2017 7 / 46
Towards Transformers
Sukant Hajra / @shajra Classy Monad Transformers (Stop Eff’ing) March 24, 2017 8 / 46
Where people come from
Enterprise Java
1trait DbConn; trait MetricsConn
3class UsersDao @Inject() (db: DbConn)
5class InsightsDao @Inject()
6(db: DbConn, metrics: MetricsConn)
8class App @Inject() (users: UsersDao, insights: InsightsDao)
Complaints
no compile-time safety
lacks composition with other FP practices
Sukant Hajra / @shajra Classy Monad Transformers (Stop Eff’ing) March 24, 2017 9 / 46
A first response
Have you tried passing a parameter to a function?
1trait DbConn; trait MetricsConn
3case class User(name: String)
4case class Insight(desc: String)
6def newUser(db: DbConn)(name: String): User = ???
8def getInsight
9(db: DbConn, metrics: MetricsConn)(user: User)
10: Insight = ???
12def runApp(db: DbConn, metrics: MetricsConn): Unit = ???
Observations
safer (no runtime reflection)
feels like “manual” dependency injection
Sukant Hajra / @shajra Classy Monad Transformers (Stop Eff’ing) March 24, 2017 10 / 46
A second response
Passing a parameter is just the “reader” monad
1case class Reader[R, A](run: R => A)
Sukant Hajra / @shajra Classy Monad Transformers (Stop Eff’ing) March 24, 2017 11 / 46
A second response
Reader’s monad instance
1implicit def readerMonad[R]: Monad[Reader[R, ?]] =
2new Monad[Reader[R, ?]] {
4def pure[A](a: A): Reader[R, A] =
5Reader { _ => a }
7override def flatMap[A, B]
8(ra: Reader[R, A])(f: A => Reader[R, B])
9: Reader[R, B] =
10Reader { r => f(ra run r) run r }
12}
Sukant Hajra / @shajra Classy Monad Transformers (Stop Eff’ing) March 24, 2017 12 / 46
A second response
1trait DbConfig; trait MetricsConfig
3case class AppConfig(db: DbConfig, metrics: MetricsConfig)
5def newUser(name: String): Reader[AppConfig, User] = ???
7def getInsight(user: User): Reader[AppConfig, Insight] = ???
9def showInsight
10(insight: Insight): Reader[AppConfig, Unit] = ???
12def app: Reader[AppConfig, Unit] =
13for {
14u <- newUser("Sukant")
15i <- getInsight(u)
16_ <- showInsight(i)
17} yield ()
Sukant Hajra / @shajra Classy Monad Transformers (Stop Eff’ing) March 24, 2017 13 / 46
A second response
Benefits
Plumbing is hidden a little.
We’re getting some composition.
Complaints
A global config is anti-modular.
Side-effects! Is this even FP?
Sukant Hajra / @shajra Classy Monad Transformers (Stop Eff’ing) March 24, 2017 14 / 46
Effect-tracking types
Naive implementation for presentation (stack unsafe)
1class IO[A](a: => A) {
2def unsafeRun: A = a
3}
5object IO { def apply[A](a: => A) = new IO(a) }
7implicit def ioMonad: Monad[IO] =
8new Monad[IO] {
9def pure[A](a: A): IO[A] = IO(a)
10override def flatMap[A, B]
11(ioa: IO[A])(f: A => IO[B]): IO[B] =
12IO(f(ioa.unsafeRun).unsafeRun)
13}
Sukant Hajra / @shajra Classy Monad Transformers (Stop Eff’ing) March 24, 2017 15 / 46
Effect-tracking types
No side-effects while composing
1def getTime: IO[Long] = IO { System.currentTimeMillis }
2def printOut[A](a: A): IO[Unit] = IO { println(a) }
4def sillyIO: IO[Unit] =
5for {
6t <- getTime
7_ <- printOut(t)
8_ <- printOut(t)
9} yield ()
Run at the “end of the world”
1scala> sillyIO.unsafeRun
21490293856842
31490293856842
Sukant Hajra / @shajra Classy Monad Transformers (Stop Eff’ing) March 24, 2017 16 / 46
Thus far we have
Two monads
Reader passes in a parameters
IO tracks an effect
Is composing them useful?
Reader[IO[A]]
Sukant Hajra / @shajra Classy Monad Transformers (Stop Eff’ing) March 24, 2017 17 / 46
Let’s compose our monads
But in general, monads don’t compose
1case class Compose[F[_], G[_], A](fga: F[G[A]])
3def impossible[F[_] : Monad, G[_] : Monad]
4: Monad[Compose[F, G, ?]] = ???
Even if we can flatten F[F[A]] and G[G[A]]
It’s hard to flatten F[G[F[G[A]]]].
Can we compose IO and Reader specifically?
Yes, that’s exactly what monad transformers do.
Sukant Hajra / @shajra Classy Monad Transformers (Stop Eff’ing) March 24, 2017 18 / 46
Many monads have respective transformers
Reader’s transformer — ReaderT
1case class ReaderT[R, M[_], A](run: R => M[A])
Sukant Hajra / @shajra Classy Monad Transformers (Stop Eff’ing) March 24, 2017 19 / 46
ReaderT’s monad instance
Depends on inner type’s monad instance
1implicit def readerTMonad[R, M[_]]
2(implicit M: Monad[M]): Monad[ReaderT[R, M, ?]] =
4new Monad[ReaderT[R, M, ?]] {
6def pure[A](a: A): ReaderT[R, M, A] =
7ReaderT { _ => M.pure(a) }
9override def flatMap[A, B]
10(ma: ReaderT[R, M, A])(f: A => ReaderT[R, M, B])
11: ReaderT[R, M, B] =
12ReaderT { r => M.flatMap(ma run r) { f(_) run r } }
14}
Sukant Hajra / @shajra Classy Monad Transformers (Stop Eff’ing) March 24, 2017 20 / 46
We can create “stacked” monads
Composing a monad stack
1type Stack[A] = ReaderT[Int, IO, A]
3val concretelyBuilt: Stack[(String, Int, Long)] =
4for {
5c <- "hi".pure[Stack]
6r <- ReaderT { (r: Int) => r.pure[IO] } // ugly
7t <- ReaderT { (_: Int) => getTime } // ugly
8} yield (c, r, t)
Running a monad stack
1scala> concretelyBuilt.run(1).unsafeRun
2res16: (String, Int, Long) = (hi,1,1490293857180)
Sukant Hajra / @shajra Classy Monad Transformers (Stop Eff’ing) March 24, 2017 21 / 46
A useful typeclass for readers
1trait MonadReader[R, M[_]] {
3def monad: Monad[M]
5def ask: M[R]
6def local[A](ma: M[A])(f: R => R): M[A]
8}
10object MonadReader {
11def ask[M[_], R](implicit MR: MonadReader[R, M]): M[R] =
12MR.ask
13}
Note: the MonadReader type class has laws
very important, but elided for time
Sukant Hajra / @shajra Classy Monad Transformers (Stop Eff’ing) March 24, 2017 22 / 46
Creating MonadReader for ReaderT
1implicit def readerTMonadReader[R, M[_]]
2(implicit M: Monad[M])
3: MonadReader[R, ReaderT[R, M, ?]] =
4new MonadReader[R, ReaderT[R, M, ?]] {
6val monad = readerTMonad(M)
8def ask: ReaderT[R, M, R] = ReaderT { _.pure[M] }
10def local[A]
11(ma: ReaderT[R, M, A])(f: R => R): ReaderT[R, M, A] =
12ReaderT { ma run f(_) }
14}
Sukant Hajra / @shajra Classy Monad Transformers (Stop Eff’ing) March 24, 2017 23 / 46
Using stacks with parametric polymorphism
Stack not specified, only constrained
1def abstractlyBuilt[M[_] : Monad : MonadReader[Int, ?[_]]]
2: M[(String, Int)] =
3for {
4c <- "hi".pure[M]
5r <- MonadReader.ask[M, Int]
7// can't do this yet
8// t <- ReaderT { (_: Int) => getTime }
10// nicer syntax would be
11// getTime.liftBase[M]
13} yield (c, r)
Stack specified when run
1scala> abstractlyBuilt[Stack].run(1).unsafeRun
2res18: (String, Int) = (hi,1)
Sukant Hajra / @shajra Classy Monad Transformers (Stop Eff’ing) March 24, 2017 24 / 46
One more useful lift
For lifting your base monad
1trait MonadBase[B[_], M[_]] {
3def monadBase: Monad[B]
4def monad: Monad[M]
6def liftBase[A](base: B[A]): M[A]
8}
Sukant Hajra / @shajra Classy Monad Transformers (Stop Eff’ing) March 24, 2017 25 / 46
A lift behind the scenes
People used to complain about this
1trait MonadTrans[T[_[_], _]] {
2def liftT[G[_] : Monad, A](a: G[A]): T[G, A]
3}
But now it can be internal plumbing
Don’t lift too much!
With the SI-2712 fix, you don’t have to
Sukant Hajra / @shajra Classy Monad Transformers (Stop Eff’ing) March 24, 2017 26 / 46
Many other transformers
Transformer Underlying Type class
IdentityT[M[_], A] M[A]
ReaderT[S, M[_], A] R =>M[A] MonadReader[R, M[_]]
StateT[S, M[_], A] S =>M[(S, A)] MonadState[S, M[_]]
OptionT[M[_], A] M[Option[A]] MonadOption[E, M[_]]
EitherT[E, M[_], A] M[Either[E,A]] MonadError[E, M[_]]
ContT[M[_], A] (A =>M[R])=>M[R] MonadCont[M[_]]
. . . . . . . . .
Sukant Hajra / @shajra Classy Monad Transformers (Stop Eff’ing) March 24, 2017 27 / 46
Some transformers commute effects
But we end up with O(n2
) to support them
1implicit def readerTMonadState[R, S, M[_]]
2(implicit MS: MonadState[S, M])
3: MonadState[S, ReaderT[R, M, ?]] =
4??? // can be implemented lawfully
Not all transformers commute effects
1implicit def contTMonadError[R, E, M[_]]
2(implicit ME: MonadError[E, M])
3: MonadError[E, ContT[M, ?]] =
4??? // would break MonadError laws if implemented
Sukant Hajra / @shajra Classy Monad Transformers (Stop Eff’ing) March 24, 2017 28 / 46
What have we got thus far?
Improvements
separations of concerns (Reader from IO)
no side-effects
Remaining Complaint
still using a global configuration
Sukant Hajra / @shajra Classy Monad Transformers (Stop Eff’ing) March 24, 2017 29 / 46
Classy Monad Transformers Example
Sukant Hajra / @shajra Classy Monad Transformers (Stop Eff’ing) March 24, 2017 30 / 46
Setup
Using a fork of Aloïs Cochard’s “scato-style” Scalaz 8
1import scalaz.Prelude.Base._
Notable differences
minimal subtyping
SI-2712 fixed!
Sukant Hajra / @shajra Classy Monad Transformers (Stop Eff’ing) March 24, 2017 31 / 46
Some abstractions
Our configuration from before
1case class DbConfig()
2case class MetricsConfig()
3case class AppConfig(db: DbConfig, metrics: MetricsConfig)
Sukant Hajra / @shajra Classy Monad Transformers (Stop Eff’ing) March 24, 2017 32 / 46
Don’t use transformer class directly
App-level type classes
1trait MonadDb[M[_]] {
2def monadBase: MonadBase[IO, M]
3def dbConfig: M[DbConfig]
4}
6trait MonadMetrics[M[_]] {
7def monadBase: MonadBase[IO, M]
8def metricsConfig: M[MetricsConfig]
9}
Sukant Hajra / @shajra Classy Monad Transformers (Stop Eff’ing) March 24, 2017 33 / 46
Scato-encoding isn’t perfect
Because we’re not subtyping
1trait AppHierarchy0 extends scalaz.BaseHierarchy {
2implicit def metricsMonadBase[M[_]]
3(implicit M: MonadMetrics[M]): MonadBase[IO, M] =
4M.monadBase
5}
7trait AppHierarchy1 extends AppHierarchy0 {
8implicit def dbMonadBase[M[_]]
9(implicit M: MonadDb[M]): MonadBase[IO, M] =
10M.monadBase
11}
13object AppHierarchy extends AppHierarchy1
14import AppHierarchy._
Sukant Hajra / @shajra Classy Monad Transformers (Stop Eff’ing) March 24, 2017 34 / 46
Make an “app” monad
Use whatever stack makes sense
1type AppStack[A] = ReaderT[AppConfig, IO, A]
2case class App[A](run: AppStack[A])
Sukant Hajra / @shajra Classy Monad Transformers (Stop Eff’ing) March 24, 2017 35 / 46
Make instances for the “app” monad
Haskell’s NewTypeDeriving would be nice here
1implicit val appInstances
2: MonadDb[App] with MonadMetrics[App] =
3new MonadDb[App] with MonadMetrics[App] {
4def monadBase =
5new MonadBaseClass[IO, App] with
6MonadClass.Template[App] with
7BindClass.Ap[App] {
8def pure[A](a: A): App[A] = App(a.pure[AppStack])
9def flatMap[A, B]
10(ma: App[A])(f: A => App[B]): App[B] =
11App(ma.run.flatMap(f andThen { _.run }))
12def liftBase[A](base: IO[A]) =
13App(base.liftBase[AppStack])
14def monadBase = Monad[IO]
15}
16def ask = MonadReader.ask[AppStack, AppConfig]
17def dbConfig = App(ask.map { _.db })
18def metricsConfig = App(ask.map { _.metrics })
19}
Sukant Hajra / @shajra Classy Monad Transformers (Stop Eff’ing) March 24, 2017 36 / 46
Write agnostic computations
Low-level IO calls
1case class User(name: String)
2case class Insight(desc: String)
4def lowLevelMakeUser
5(db: DbConfig, name: String): IO[User] =
6User(name).pure[IO]
8def lowLevelGetInsight
9(db: DbConfig, metrics: MetricsConfig, user: User)
10: IO[Insight] =
11IO {
12val username = System getenv "USER"
13Insight(s"likes username: ${username}")
14}
Sukant Hajra / @shajra Classy Monad Transformers (Stop Eff’ing) March 24, 2017 37 / 46
Write agnostic computations
Mid-level composition
1def makeUser[M[_]]
2(name: String)(implicit D: MonadDb[M]): M[User] =
3for {
4conf <- D.dbConfig
5user <- lowLevelMakeUser(conf, name).liftBase[M]
6} yield user
8def getInsight[M[_]]
9(user: User)(implicit D: MonadDb[M], M: MonadMetrics[M])
10: M[Insight] =
11for {
12db <- D.dbConfig
13metrics <- M.metricsConfig
14insight <-
15lowLevelGetInsight(db, metrics, user).liftBase[M]
16} yield insight
Sukant Hajra / @shajra Classy Monad Transformers (Stop Eff’ing) March 24, 2017 38 / 46
Write agnostic computations
One more mid-level
1def showInsight[M[_] : MonadBase[IO, ?[_]]]
2(user: User, insight: Insight): M[Unit] =
3IO{ println(s"${user.name} ${insight.desc}") }.liftBase[M]
Our final application
1def app[M[_] : MonadDb : MonadMetrics]: M[Unit] =
2for {
3user <- makeUser[M]("Sukant")
4insight <- getInsight[M](user)
5_ <- showInsight[M](user, insight)
6} yield ()
Sukant Hajra / @shajra Classy Monad Transformers (Stop Eff’ing) March 24, 2017 39 / 46
The end of the world
1val conf = AppConfig(DbConfig(), MetricsConfig())
1scala> app[App].run.run(conf).unsafeRun
2Sukant likes username: shajra
Sukant Hajra / @shajra Classy Monad Transformers (Stop Eff’ing) March 24, 2017 40 / 46
Effect Commutativity
Sukant Hajra / @shajra Classy Monad Transformers (Stop Eff’ing) March 24, 2017 41 / 46
We don’t have to specify a stack order
1type StateEff[F[_]] = MonadState[Int, F]
2type ErrorEff[F[_]] = MonadError[String, F]
4def tryMtl[F[_] : ErrorEff : StateEff] = {
5val attempt =
6for {
7_ <- 1.put[F]
8_ <- "oh noes".raiseError[F, Unit]
9} yield ()
10attempt.handleError(_ => ().pure[F])
11}
Sukant Hajra / @shajra Classy Monad Transformers (Stop Eff’ing) March 24, 2017 42 / 46
Different stacks, different results
Stack Representation
StateT[Int, EitherT[String, Identity, ?], A] S =>M[Either[E, (S, A)]]
EitherT[String, StateT[Int, Identity, ?], A] S =>M[(S, Either[E, A])]
1type StackSE[A] =
2StateT[Int, EitherT[String, Identity, ?], A]
3type StackES[A] =
4EitherT[String, StateT[Int, Identity, ?], A]
1scala> tryMtl[StackSE].run(0).run.run
2res1: scalaz./[String,(Int, Unit)] = /-((0,()))
4scala> tryMtl[StackES].run.run(0).run
5res2: (Int, scalaz./[String,Unit]) = (1,/-(()))
Sukant Hajra / @shajra Classy Monad Transformers (Stop Eff’ing) March 24, 2017 43 / 46
Trying with Eff
1import cats.data._; import cats.syntax.all._
2import org.atnos.eff._; import org.atnos.eff.all._
3import org.atnos.eff.syntax.all._
5type StateEff[A] = State[Int, A]
6type ErrorEff[A] = Either[String, A]
8def tryEff[R]
9(implicit S: StateEff |= R , W: ErrorEff /= R)
10: Eff[R, Unit] = {
11val attempt: Eff[R, Unit] =
12for {
13_ <- put(1)
14_ <- left[R, String, Unit]("oh noes")
15} yield ()
16catchLeft[R, String, Unit](attempt) { _ =>
17().pure[Eff[R, ?]]
18}
19}
Sukant Hajra / @shajra Classy Monad Transformers (Stop Eff’ing) March 24, 2017 44 / 46
By default, interpretation locked in
1scala> type StackSE = Fx.fx2[StateEff, ErrorEff]
2defined type alias StackSE
4scala> type StackES = Fx.fx2[ErrorEff, StateEff]
5defined type alias StackES
7scala> tryEff[StackSE].runState(0).runEither.run
8res2: scala.util.Either[String,(Unit, Int)] = Right(((),1))
10scala> tryEff[StackSE].runEither.runState(0).run
11res3: (scala.util.Either[String,Unit], Int) = (Right(()),1)
13scala> tryEff[StackES].runState(0).runEither.run
14res4: scala.util.Either[String,(Unit, Int)] = Right(((),1))
16scala> tryEff[StackES].runEither.runState(0).run
17res5: (scala.util.Either[String,Unit], Int) = (Right(()),1)
Sukant Hajra / @shajra Classy Monad Transformers (Stop Eff’ing) March 24, 2017 45 / 46
Wrapping up
Sukant Hajra / @shajra Classy Monad Transformers (Stop Eff’ing) March 24, 2017 46 / 46

Classy Monad Transformers (Stop Eff'ing Around)

  • 1.
    Classy Monad Transformers(Stop Eff’ing) Sukant Hajra / @shajra March 24, 2017 Sukant Hajra / @shajra Classy Monad Transformers (Stop Eff’ing) March 24, 2017 1 / 46
  • 2.
    Goals introduce monad transformers illustrateergonomics in Scala recommend a usage begin comparison with alternatives Sukant Hajra / @shajra Classy Monad Transformers (Stop Eff’ing) March 24, 2017 2 / 46
  • 3.
    Quick mention Figure 1:My Employer Sukant Hajra / @shajra Classy Monad Transformers (Stop Eff’ing) March 24, 2017 3 / 46
  • 4.
    Materials This presentation andall code is at github.com/shajra/shajra-presentations/tree/master/scala-mtl compiler-checked by Rob Norris’s sbt-tut plugin. Sukant Hajra / @shajra Classy Monad Transformers (Stop Eff’ing) March 24, 2017 4 / 46
  • 5.
    In lieu oftime Assuming knowledge of Scala implicits type classes for-yield sugar w.r.t. Monad. Sukant Hajra / @shajra Classy Monad Transformers (Stop Eff’ing) March 24, 2017 5 / 46
  • 6.
    Monads, Explicitly 1trait Monad[M[_]]{ 3def pure[A](a: A): M[A] 5def flatMap[A, B](ma: M[A])(f: A => M[B]): M[B] = 6flatten(map(ma)(f)) 8def flatten[A](mma: M[M[A]]): M[A] = 9flatMap(mma)(identity) 11def map[A, B](ma: M[A])(f: A => B): M[B] = 12flatMap(ma)(f andThen pure) 14} Note: the Monad type class has three laws very important, but elided for time Sukant Hajra / @shajra Classy Monad Transformers (Stop Eff’ing) March 24, 2017 6 / 46
  • 7.
    Monad syntax withimplicits For convenience (e.g. with for-yield) 1implicit class OpsA[A](a: A) { 3def pure[M[_]](implicit M: Monad[M]): M[A] = 4M pure a 6} 8implicit class 9MonadOps[M[_], A](ma: M[A])(implicit M: Monad[M]) { 11def map[B](f: A => B): M[B] = 12M.map(ma)(f) 14def flatMap[B](f: A => M[B]): M[B] = 15M.flatMap(ma)(f) 17} Sukant Hajra / @shajra Classy Monad Transformers (Stop Eff’ing) March 24, 2017 7 / 46
  • 8.
    Towards Transformers Sukant Hajra/ @shajra Classy Monad Transformers (Stop Eff’ing) March 24, 2017 8 / 46
  • 9.
    Where people comefrom Enterprise Java 1trait DbConn; trait MetricsConn 3class UsersDao @Inject() (db: DbConn) 5class InsightsDao @Inject() 6(db: DbConn, metrics: MetricsConn) 8class App @Inject() (users: UsersDao, insights: InsightsDao) Complaints no compile-time safety lacks composition with other FP practices Sukant Hajra / @shajra Classy Monad Transformers (Stop Eff’ing) March 24, 2017 9 / 46
  • 10.
    A first response Haveyou tried passing a parameter to a function? 1trait DbConn; trait MetricsConn 3case class User(name: String) 4case class Insight(desc: String) 6def newUser(db: DbConn)(name: String): User = ??? 8def getInsight 9(db: DbConn, metrics: MetricsConn)(user: User) 10: Insight = ??? 12def runApp(db: DbConn, metrics: MetricsConn): Unit = ??? Observations safer (no runtime reflection) feels like “manual” dependency injection Sukant Hajra / @shajra Classy Monad Transformers (Stop Eff’ing) March 24, 2017 10 / 46
  • 11.
    A second response Passinga parameter is just the “reader” monad 1case class Reader[R, A](run: R => A) Sukant Hajra / @shajra Classy Monad Transformers (Stop Eff’ing) March 24, 2017 11 / 46
  • 12.
    A second response Reader’smonad instance 1implicit def readerMonad[R]: Monad[Reader[R, ?]] = 2new Monad[Reader[R, ?]] { 4def pure[A](a: A): Reader[R, A] = 5Reader { _ => a } 7override def flatMap[A, B] 8(ra: Reader[R, A])(f: A => Reader[R, B]) 9: Reader[R, B] = 10Reader { r => f(ra run r) run r } 12} Sukant Hajra / @shajra Classy Monad Transformers (Stop Eff’ing) March 24, 2017 12 / 46
  • 13.
    A second response 1traitDbConfig; trait MetricsConfig 3case class AppConfig(db: DbConfig, metrics: MetricsConfig) 5def newUser(name: String): Reader[AppConfig, User] = ??? 7def getInsight(user: User): Reader[AppConfig, Insight] = ??? 9def showInsight 10(insight: Insight): Reader[AppConfig, Unit] = ??? 12def app: Reader[AppConfig, Unit] = 13for { 14u <- newUser("Sukant") 15i <- getInsight(u) 16_ <- showInsight(i) 17} yield () Sukant Hajra / @shajra Classy Monad Transformers (Stop Eff’ing) March 24, 2017 13 / 46
  • 14.
    A second response Benefits Plumbingis hidden a little. We’re getting some composition. Complaints A global config is anti-modular. Side-effects! Is this even FP? Sukant Hajra / @shajra Classy Monad Transformers (Stop Eff’ing) March 24, 2017 14 / 46
  • 15.
    Effect-tracking types Naive implementationfor presentation (stack unsafe) 1class IO[A](a: => A) { 2def unsafeRun: A = a 3} 5object IO { def apply[A](a: => A) = new IO(a) } 7implicit def ioMonad: Monad[IO] = 8new Monad[IO] { 9def pure[A](a: A): IO[A] = IO(a) 10override def flatMap[A, B] 11(ioa: IO[A])(f: A => IO[B]): IO[B] = 12IO(f(ioa.unsafeRun).unsafeRun) 13} Sukant Hajra / @shajra Classy Monad Transformers (Stop Eff’ing) March 24, 2017 15 / 46
  • 16.
    Effect-tracking types No side-effectswhile composing 1def getTime: IO[Long] = IO { System.currentTimeMillis } 2def printOut[A](a: A): IO[Unit] = IO { println(a) } 4def sillyIO: IO[Unit] = 5for { 6t <- getTime 7_ <- printOut(t) 8_ <- printOut(t) 9} yield () Run at the “end of the world” 1scala> sillyIO.unsafeRun 21490293856842 31490293856842 Sukant Hajra / @shajra Classy Monad Transformers (Stop Eff’ing) March 24, 2017 16 / 46
  • 17.
    Thus far wehave Two monads Reader passes in a parameters IO tracks an effect Is composing them useful? Reader[IO[A]] Sukant Hajra / @shajra Classy Monad Transformers (Stop Eff’ing) March 24, 2017 17 / 46
  • 18.
    Let’s compose ourmonads But in general, monads don’t compose 1case class Compose[F[_], G[_], A](fga: F[G[A]]) 3def impossible[F[_] : Monad, G[_] : Monad] 4: Monad[Compose[F, G, ?]] = ??? Even if we can flatten F[F[A]] and G[G[A]] It’s hard to flatten F[G[F[G[A]]]]. Can we compose IO and Reader specifically? Yes, that’s exactly what monad transformers do. Sukant Hajra / @shajra Classy Monad Transformers (Stop Eff’ing) March 24, 2017 18 / 46
  • 19.
    Many monads haverespective transformers Reader’s transformer — ReaderT 1case class ReaderT[R, M[_], A](run: R => M[A]) Sukant Hajra / @shajra Classy Monad Transformers (Stop Eff’ing) March 24, 2017 19 / 46
  • 20.
    ReaderT’s monad instance Dependson inner type’s monad instance 1implicit def readerTMonad[R, M[_]] 2(implicit M: Monad[M]): Monad[ReaderT[R, M, ?]] = 4new Monad[ReaderT[R, M, ?]] { 6def pure[A](a: A): ReaderT[R, M, A] = 7ReaderT { _ => M.pure(a) } 9override def flatMap[A, B] 10(ma: ReaderT[R, M, A])(f: A => ReaderT[R, M, B]) 11: ReaderT[R, M, B] = 12ReaderT { r => M.flatMap(ma run r) { f(_) run r } } 14} Sukant Hajra / @shajra Classy Monad Transformers (Stop Eff’ing) March 24, 2017 20 / 46
  • 21.
    We can create“stacked” monads Composing a monad stack 1type Stack[A] = ReaderT[Int, IO, A] 3val concretelyBuilt: Stack[(String, Int, Long)] = 4for { 5c <- "hi".pure[Stack] 6r <- ReaderT { (r: Int) => r.pure[IO] } // ugly 7t <- ReaderT { (_: Int) => getTime } // ugly 8} yield (c, r, t) Running a monad stack 1scala> concretelyBuilt.run(1).unsafeRun 2res16: (String, Int, Long) = (hi,1,1490293857180) Sukant Hajra / @shajra Classy Monad Transformers (Stop Eff’ing) March 24, 2017 21 / 46
  • 22.
    A useful typeclassfor readers 1trait MonadReader[R, M[_]] { 3def monad: Monad[M] 5def ask: M[R] 6def local[A](ma: M[A])(f: R => R): M[A] 8} 10object MonadReader { 11def ask[M[_], R](implicit MR: MonadReader[R, M]): M[R] = 12MR.ask 13} Note: the MonadReader type class has laws very important, but elided for time Sukant Hajra / @shajra Classy Monad Transformers (Stop Eff’ing) March 24, 2017 22 / 46
  • 23.
    Creating MonadReader forReaderT 1implicit def readerTMonadReader[R, M[_]] 2(implicit M: Monad[M]) 3: MonadReader[R, ReaderT[R, M, ?]] = 4new MonadReader[R, ReaderT[R, M, ?]] { 6val monad = readerTMonad(M) 8def ask: ReaderT[R, M, R] = ReaderT { _.pure[M] } 10def local[A] 11(ma: ReaderT[R, M, A])(f: R => R): ReaderT[R, M, A] = 12ReaderT { ma run f(_) } 14} Sukant Hajra / @shajra Classy Monad Transformers (Stop Eff’ing) March 24, 2017 23 / 46
  • 24.
    Using stacks withparametric polymorphism Stack not specified, only constrained 1def abstractlyBuilt[M[_] : Monad : MonadReader[Int, ?[_]]] 2: M[(String, Int)] = 3for { 4c <- "hi".pure[M] 5r <- MonadReader.ask[M, Int] 7// can't do this yet 8// t <- ReaderT { (_: Int) => getTime } 10// nicer syntax would be 11// getTime.liftBase[M] 13} yield (c, r) Stack specified when run 1scala> abstractlyBuilt[Stack].run(1).unsafeRun 2res18: (String, Int) = (hi,1) Sukant Hajra / @shajra Classy Monad Transformers (Stop Eff’ing) March 24, 2017 24 / 46
  • 25.
    One more usefullift For lifting your base monad 1trait MonadBase[B[_], M[_]] { 3def monadBase: Monad[B] 4def monad: Monad[M] 6def liftBase[A](base: B[A]): M[A] 8} Sukant Hajra / @shajra Classy Monad Transformers (Stop Eff’ing) March 24, 2017 25 / 46
  • 26.
    A lift behindthe scenes People used to complain about this 1trait MonadTrans[T[_[_], _]] { 2def liftT[G[_] : Monad, A](a: G[A]): T[G, A] 3} But now it can be internal plumbing Don’t lift too much! With the SI-2712 fix, you don’t have to Sukant Hajra / @shajra Classy Monad Transformers (Stop Eff’ing) March 24, 2017 26 / 46
  • 27.
    Many other transformers TransformerUnderlying Type class IdentityT[M[_], A] M[A] ReaderT[S, M[_], A] R =>M[A] MonadReader[R, M[_]] StateT[S, M[_], A] S =>M[(S, A)] MonadState[S, M[_]] OptionT[M[_], A] M[Option[A]] MonadOption[E, M[_]] EitherT[E, M[_], A] M[Either[E,A]] MonadError[E, M[_]] ContT[M[_], A] (A =>M[R])=>M[R] MonadCont[M[_]] . . . . . . . . . Sukant Hajra / @shajra Classy Monad Transformers (Stop Eff’ing) March 24, 2017 27 / 46
  • 28.
    Some transformers commuteeffects But we end up with O(n2 ) to support them 1implicit def readerTMonadState[R, S, M[_]] 2(implicit MS: MonadState[S, M]) 3: MonadState[S, ReaderT[R, M, ?]] = 4??? // can be implemented lawfully Not all transformers commute effects 1implicit def contTMonadError[R, E, M[_]] 2(implicit ME: MonadError[E, M]) 3: MonadError[E, ContT[M, ?]] = 4??? // would break MonadError laws if implemented Sukant Hajra / @shajra Classy Monad Transformers (Stop Eff’ing) March 24, 2017 28 / 46
  • 29.
    What have wegot thus far? Improvements separations of concerns (Reader from IO) no side-effects Remaining Complaint still using a global configuration Sukant Hajra / @shajra Classy Monad Transformers (Stop Eff’ing) March 24, 2017 29 / 46
  • 30.
    Classy Monad TransformersExample Sukant Hajra / @shajra Classy Monad Transformers (Stop Eff’ing) March 24, 2017 30 / 46
  • 31.
    Setup Using a forkof Aloïs Cochard’s “scato-style” Scalaz 8 1import scalaz.Prelude.Base._ Notable differences minimal subtyping SI-2712 fixed! Sukant Hajra / @shajra Classy Monad Transformers (Stop Eff’ing) March 24, 2017 31 / 46
  • 32.
    Some abstractions Our configurationfrom before 1case class DbConfig() 2case class MetricsConfig() 3case class AppConfig(db: DbConfig, metrics: MetricsConfig) Sukant Hajra / @shajra Classy Monad Transformers (Stop Eff’ing) March 24, 2017 32 / 46
  • 33.
    Don’t use transformerclass directly App-level type classes 1trait MonadDb[M[_]] { 2def monadBase: MonadBase[IO, M] 3def dbConfig: M[DbConfig] 4} 6trait MonadMetrics[M[_]] { 7def monadBase: MonadBase[IO, M] 8def metricsConfig: M[MetricsConfig] 9} Sukant Hajra / @shajra Classy Monad Transformers (Stop Eff’ing) March 24, 2017 33 / 46
  • 34.
    Scato-encoding isn’t perfect Becausewe’re not subtyping 1trait AppHierarchy0 extends scalaz.BaseHierarchy { 2implicit def metricsMonadBase[M[_]] 3(implicit M: MonadMetrics[M]): MonadBase[IO, M] = 4M.monadBase 5} 7trait AppHierarchy1 extends AppHierarchy0 { 8implicit def dbMonadBase[M[_]] 9(implicit M: MonadDb[M]): MonadBase[IO, M] = 10M.monadBase 11} 13object AppHierarchy extends AppHierarchy1 14import AppHierarchy._ Sukant Hajra / @shajra Classy Monad Transformers (Stop Eff’ing) March 24, 2017 34 / 46
  • 35.
    Make an “app”monad Use whatever stack makes sense 1type AppStack[A] = ReaderT[AppConfig, IO, A] 2case class App[A](run: AppStack[A]) Sukant Hajra / @shajra Classy Monad Transformers (Stop Eff’ing) March 24, 2017 35 / 46
  • 36.
    Make instances forthe “app” monad Haskell’s NewTypeDeriving would be nice here 1implicit val appInstances 2: MonadDb[App] with MonadMetrics[App] = 3new MonadDb[App] with MonadMetrics[App] { 4def monadBase = 5new MonadBaseClass[IO, App] with 6MonadClass.Template[App] with 7BindClass.Ap[App] { 8def pure[A](a: A): App[A] = App(a.pure[AppStack]) 9def flatMap[A, B] 10(ma: App[A])(f: A => App[B]): App[B] = 11App(ma.run.flatMap(f andThen { _.run })) 12def liftBase[A](base: IO[A]) = 13App(base.liftBase[AppStack]) 14def monadBase = Monad[IO] 15} 16def ask = MonadReader.ask[AppStack, AppConfig] 17def dbConfig = App(ask.map { _.db }) 18def metricsConfig = App(ask.map { _.metrics }) 19} Sukant Hajra / @shajra Classy Monad Transformers (Stop Eff’ing) March 24, 2017 36 / 46
  • 37.
    Write agnostic computations Low-levelIO calls 1case class User(name: String) 2case class Insight(desc: String) 4def lowLevelMakeUser 5(db: DbConfig, name: String): IO[User] = 6User(name).pure[IO] 8def lowLevelGetInsight 9(db: DbConfig, metrics: MetricsConfig, user: User) 10: IO[Insight] = 11IO { 12val username = System getenv "USER" 13Insight(s"likes username: ${username}") 14} Sukant Hajra / @shajra Classy Monad Transformers (Stop Eff’ing) March 24, 2017 37 / 46
  • 38.
    Write agnostic computations Mid-levelcomposition 1def makeUser[M[_]] 2(name: String)(implicit D: MonadDb[M]): M[User] = 3for { 4conf <- D.dbConfig 5user <- lowLevelMakeUser(conf, name).liftBase[M] 6} yield user 8def getInsight[M[_]] 9(user: User)(implicit D: MonadDb[M], M: MonadMetrics[M]) 10: M[Insight] = 11for { 12db <- D.dbConfig 13metrics <- M.metricsConfig 14insight <- 15lowLevelGetInsight(db, metrics, user).liftBase[M] 16} yield insight Sukant Hajra / @shajra Classy Monad Transformers (Stop Eff’ing) March 24, 2017 38 / 46
  • 39.
    Write agnostic computations Onemore mid-level 1def showInsight[M[_] : MonadBase[IO, ?[_]]] 2(user: User, insight: Insight): M[Unit] = 3IO{ println(s"${user.name} ${insight.desc}") }.liftBase[M] Our final application 1def app[M[_] : MonadDb : MonadMetrics]: M[Unit] = 2for { 3user <- makeUser[M]("Sukant") 4insight <- getInsight[M](user) 5_ <- showInsight[M](user, insight) 6} yield () Sukant Hajra / @shajra Classy Monad Transformers (Stop Eff’ing) March 24, 2017 39 / 46
  • 40.
    The end ofthe world 1val conf = AppConfig(DbConfig(), MetricsConfig()) 1scala> app[App].run.run(conf).unsafeRun 2Sukant likes username: shajra Sukant Hajra / @shajra Classy Monad Transformers (Stop Eff’ing) March 24, 2017 40 / 46
  • 41.
    Effect Commutativity Sukant Hajra/ @shajra Classy Monad Transformers (Stop Eff’ing) March 24, 2017 41 / 46
  • 42.
    We don’t haveto specify a stack order 1type StateEff[F[_]] = MonadState[Int, F] 2type ErrorEff[F[_]] = MonadError[String, F] 4def tryMtl[F[_] : ErrorEff : StateEff] = { 5val attempt = 6for { 7_ <- 1.put[F] 8_ <- "oh noes".raiseError[F, Unit] 9} yield () 10attempt.handleError(_ => ().pure[F]) 11} Sukant Hajra / @shajra Classy Monad Transformers (Stop Eff’ing) March 24, 2017 42 / 46
  • 43.
    Different stacks, differentresults Stack Representation StateT[Int, EitherT[String, Identity, ?], A] S =>M[Either[E, (S, A)]] EitherT[String, StateT[Int, Identity, ?], A] S =>M[(S, Either[E, A])] 1type StackSE[A] = 2StateT[Int, EitherT[String, Identity, ?], A] 3type StackES[A] = 4EitherT[String, StateT[Int, Identity, ?], A] 1scala> tryMtl[StackSE].run(0).run.run 2res1: scalaz./[String,(Int, Unit)] = /-((0,())) 4scala> tryMtl[StackES].run.run(0).run 5res2: (Int, scalaz./[String,Unit]) = (1,/-(())) Sukant Hajra / @shajra Classy Monad Transformers (Stop Eff’ing) March 24, 2017 43 / 46
  • 44.
    Trying with Eff 1importcats.data._; import cats.syntax.all._ 2import org.atnos.eff._; import org.atnos.eff.all._ 3import org.atnos.eff.syntax.all._ 5type StateEff[A] = State[Int, A] 6type ErrorEff[A] = Either[String, A] 8def tryEff[R] 9(implicit S: StateEff |= R , W: ErrorEff /= R) 10: Eff[R, Unit] = { 11val attempt: Eff[R, Unit] = 12for { 13_ <- put(1) 14_ <- left[R, String, Unit]("oh noes") 15} yield () 16catchLeft[R, String, Unit](attempt) { _ => 17().pure[Eff[R, ?]] 18} 19} Sukant Hajra / @shajra Classy Monad Transformers (Stop Eff’ing) March 24, 2017 44 / 46
  • 45.
    By default, interpretationlocked in 1scala> type StackSE = Fx.fx2[StateEff, ErrorEff] 2defined type alias StackSE 4scala> type StackES = Fx.fx2[ErrorEff, StateEff] 5defined type alias StackES 7scala> tryEff[StackSE].runState(0).runEither.run 8res2: scala.util.Either[String,(Unit, Int)] = Right(((),1)) 10scala> tryEff[StackSE].runEither.runState(0).run 11res3: (scala.util.Either[String,Unit], Int) = (Right(()),1) 13scala> tryEff[StackES].runState(0).runEither.run 14res4: scala.util.Either[String,(Unit, Int)] = Right(((),1)) 16scala> tryEff[StackES].runEither.runState(0).run 17res5: (scala.util.Either[String,Unit], Int) = (Right(()),1) Sukant Hajra / @shajra Classy Monad Transformers (Stop Eff’ing) March 24, 2017 45 / 46
  • 46.
    Wrapping up Sukant Hajra/ @shajra Classy Monad Transformers (Stop Eff’ing) March 24, 2017 46 / 46