Upload
sukanthajra
View
251
Download
2
Embed Size (px)
Citation preview
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 transformersillustrate ergonomics in Scalarecommend a usagebegin 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 isat github.com/shajra/shajra-presentations/tree/master/scala-mtlcompiler-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 ofScala implicitstype classesfor-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 lawsvery 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 class9MonadOps[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 Java1trait DbConn; trait MetricsConn
3class UsersDao @Inject() (db: DbConn)
5class InsightsDao @Inject()6(db: DbConn, metrics: MetricsConn)
8class App @Inject() (users: UsersDao, insights: InsightsDao)
Complaintsno compile-time safetylacks 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 getInsight9(db: DbConn, metrics: MetricsConn)(user: User)10: Insight = ???
12def runApp(db: DbConn, metrics: MetricsConn): Unit = ???
Observationssafer (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” monad1case 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 instance1implicit 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 showInsight10(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
BenefitsPlumbing is hidden a little.We’re getting some composition.
ComplaintsA 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 = a3}
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 composing1def getTime: IO[Long] = IO { System.currentTimeMillis }2def printOut[A](a: A): IO[Unit] = IO { println(a) }
4def sillyIO: IO[Unit] =5for {6t <- getTime7_ <- printOut(t)8_ <- printOut(t)9} yield ()
Run at the “end of the world”1scala> sillyIO.unsafeRun2149029385684231490293856842
Sukant Hajra / @shajra Classy Monad Transformers (Stop Eff’ing) March 24, 2017 16 / 46
Thus far we have
Two monadsReader 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 compose1case 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 instance1implicit 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 stack1type Stack[A] = ReaderT[Int, IO, A]
3val concretelyBuilt: Stack[(String, Int, Long)] =4for {5c <- "hi".pure[Stack]6r <- ReaderT { (r: Int) => r.pure[IO] } // ugly7t <- ReaderT { (_: Int) => getTime } // ugly8} yield (c, r, t)
Running a monad stack1scala> concretelyBuilt.run(1).unsafeRun2res16: (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.ask13}
Note: the MonadReader type class has lawsvery 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 polymorphismStack 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 yet8// t <- ReaderT { (_: Int) => getTime }
10// nicer syntax would be11// getTime.liftBase[M]
13} yield (c, r)
Stack specified when run1scala> abstractlyBuilt[Stack].run(1).unsafeRun2res18: (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 monad1trait 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 this1trait MonadTrans[T[_[_], _]] {2def liftT[G[_] : Monad, A](a: G[A]): T[G, A]3}
But now it can be internal plumbingDon’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 them1implicit 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 effects1implicit 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?
Improvementsseparations of concerns (Reader from IO)no side-effects
Remaining Complaintstill 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 81import scalaz.Prelude.Base._
Notable differencesminimal subtypingSI-2712 fixed!
Sukant Hajra / @shajra Classy Monad Transformers (Stop Eff’ing) March 24, 2017 31 / 46
Some abstractions
Our configuration from before1case 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 classes1trait 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 subtyping1trait AppHierarchy0 extends scalaz.BaseHierarchy {2implicit def metricsMonadBase[M[_]]3(implicit M: MonadMetrics[M]): MonadBase[IO, M] =4M.monadBase5}
7trait AppHierarchy1 extends AppHierarchy0 {8implicit def dbMonadBase[M[_]]9(implicit M: MonadDb[M]): MonadBase[IO, M] =10M.monadBase11}
13object AppHierarchy extends AppHierarchy114import AppHierarchy._
Sukant Hajra / @shajra Classy Monad Transformers (Stop Eff’ing) March 24, 2017 34 / 46
Make an “app” monad
Use whatever stack makes sense1type 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” monadHaskell’s NewTypeDeriving would be nice here
1implicit val appInstances2: MonadDb[App] with MonadMetrics[App] =3new MonadDb[App] with MonadMetrics[App] {4def monadBase =5new MonadBaseClass[IO, App] with6MonadClass.Template[App] with7BindClass.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 calls1case class User(name: String)2case class Insight(desc: String)
4def lowLevelMakeUser5(db: DbConfig, name: String): IO[User] =6User(name).pure[IO]
8def lowLevelGetInsight9(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 composition1def makeUser[M[_]]2(name: String)(implicit D: MonadDb[M]): M[User] =3for {4conf <- D.dbConfig5user <- 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.dbConfig13metrics <- M.metricsConfig14insight <-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-level1def showInsight[M[_] : MonadBase[IO, ?[_]]]2(user: User, insight: Insight): M[Unit] =3IO{ println(s"${user.name} ${insight.desc}") }.liftBase[M]
Our final application1def 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).unsafeRun2Sukant 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.run2res1: scalaz.\/[String,(Int, Unit)] = \/-((0,()))
4scala> tryMtl[StackES].run.run(0).run5res2: (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.run8res2: scala.util.Either[String,(Unit, Int)] = Right(((),1))
10scala> tryEff[StackSE].runEither.runState(0).run11res3: (scala.util.Either[String,Unit], Int) = (Right(()),1)
13scala> tryEff[StackES].runState(0).runEither.run14res4: scala.util.Either[String,(Unit, Int)] = Right(((),1))
16scala> tryEff[StackES].runEither.runState(0).run17res5: (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