This document introduces monad transformers and illustrates their usage in Scala. It defines the Reader and IO monads and shows how they can be composed using the ReaderT transformer. This allows defining computations that both read configuration values and perform side effects. The document recommends using type classes like MonadReader and MonadBase to define computations over monad stacks in a parametrically polymorphic way. It provides examples of defining and running such computations over a ReaderT[IO, ?] stack.
4. 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
5. 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
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 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
9. 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
10. 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
11. 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
12. 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
14. 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
15. 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
16. 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
17. 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
18. 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
19. 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
20. 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
22. 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
23. 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
24. 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
25. 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
26. 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
27. 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
28. 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
29. 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
30. Classy Monad Transformers Example
Sukant Hajra / @shajra Classy Monad Transformers (Stop Eff’ing) March 24, 2017 30 / 46
31. 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
32. 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
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 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