46
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

Classy Monad Transformers (Stop Eff'ing Around)

Embed Size (px)

Citation preview

Page 1: Classy Monad Transformers (Stop Eff'ing Around)

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

Page 2: Classy Monad Transformers (Stop Eff'ing Around)

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

Page 3: Classy Monad Transformers (Stop Eff'ing Around)

Quick mention

Figure 1: My Employer

Sukant Hajra / @shajra Classy Monad Transformers (Stop Eff’ing) March 24, 2017 3 / 46

Page 4: Classy Monad Transformers (Stop Eff'ing Around)

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

Page 5: Classy Monad Transformers (Stop Eff'ing Around)

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

Page 6: Classy Monad Transformers (Stop Eff'ing Around)

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

Page 7: Classy Monad Transformers (Stop Eff'ing Around)

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

Page 8: Classy Monad Transformers (Stop Eff'ing Around)

Towards Transformers

Sukant Hajra / @shajra Classy Monad Transformers (Stop Eff’ing) March 24, 2017 8 / 46

Page 9: Classy Monad Transformers (Stop Eff'ing Around)

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

Page 10: Classy Monad Transformers (Stop Eff'ing Around)

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

Page 11: Classy Monad Transformers (Stop Eff'ing Around)

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

Page 12: Classy Monad Transformers (Stop Eff'ing Around)

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

Page 13: Classy Monad Transformers (Stop Eff'ing Around)

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

Page 14: Classy Monad Transformers (Stop Eff'ing Around)

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

Page 15: Classy Monad Transformers (Stop Eff'ing Around)

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

Page 16: Classy Monad Transformers (Stop Eff'ing Around)

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

Page 17: Classy Monad Transformers (Stop Eff'ing Around)

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

Page 18: Classy Monad Transformers (Stop Eff'ing Around)

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

Page 19: Classy Monad Transformers (Stop Eff'ing Around)

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

Page 20: Classy Monad Transformers (Stop Eff'ing Around)

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

Page 21: Classy Monad Transformers (Stop Eff'ing Around)

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

Page 22: Classy Monad Transformers (Stop Eff'ing Around)

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

Page 23: Classy Monad Transformers (Stop Eff'ing Around)

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

Page 24: Classy Monad Transformers (Stop Eff'ing Around)

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

Page 25: Classy Monad Transformers (Stop Eff'ing Around)

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

Page 26: Classy Monad Transformers (Stop Eff'ing Around)

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

Page 27: Classy Monad Transformers (Stop Eff'ing Around)

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

Page 28: Classy Monad Transformers (Stop Eff'ing Around)

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

Page 29: Classy Monad Transformers (Stop Eff'ing Around)

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

Page 30: Classy Monad Transformers (Stop Eff'ing Around)

Classy Monad Transformers Example

Sukant Hajra / @shajra Classy Monad Transformers (Stop Eff’ing) March 24, 2017 30 / 46

Page 31: Classy Monad Transformers (Stop Eff'ing Around)

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

Page 32: Classy Monad Transformers (Stop Eff'ing Around)

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

Page 33: Classy Monad Transformers (Stop Eff'ing Around)

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

Page 34: Classy Monad Transformers (Stop Eff'ing Around)

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

Page 35: Classy Monad Transformers (Stop Eff'ing Around)

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

Page 36: Classy Monad Transformers (Stop Eff'ing Around)

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

Page 37: Classy Monad Transformers (Stop Eff'ing Around)

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

Page 38: Classy Monad Transformers (Stop Eff'ing Around)

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

Page 39: Classy Monad Transformers (Stop Eff'ing Around)

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

Page 40: Classy Monad Transformers (Stop Eff'ing Around)

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

Page 41: Classy Monad Transformers (Stop Eff'ing Around)

Effect Commutativity

Sukant Hajra / @shajra Classy Monad Transformers (Stop Eff’ing) March 24, 2017 41 / 46

Page 42: Classy Monad Transformers (Stop Eff'ing Around)

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

Page 43: Classy Monad Transformers (Stop Eff'ing Around)

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

Page 44: Classy Monad Transformers (Stop Eff'ing Around)

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

Page 45: Classy Monad Transformers (Stop Eff'ing Around)

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

Page 46: Classy Monad Transformers (Stop Eff'ing Around)

Wrapping up

Sukant Hajra / @shajra Classy Monad Transformers (Stop Eff’ing) March 24, 2017 46 / 46