Successfully reported this slideshow.
We use your LinkedIn profile and activity data to personalize ads and to show you more relevant ads. You can change your ad preferences anytime.

One Monad to Rule Them All

For the past few years in the functional Scala community, the standard approach for adding features to an effect type (features like logging, stateful updates, or accessing config) has been Monad Transformers (EItherT, OptionT, WriterT, ReaderT, etc.).

While elegant and proven, monad transformers were imported directly from Haskell, and in Scala, they have poor ergonomics and poor performance. Using tagless-final on transformers can eliminate some of the boilerplate, but cannot improve performance, and tagless-final makes it insanely hard to locally introduce and eliminate features.

In this presentation, John will introduce an alternate approach he coined ‘effect rotation’, which shares most of the power of monad transformers, but with better ergonomics and no loss of performance. You will see how to use the ZIO library that John created to composably add different features into the ZIO effect type, to solve the same problems as monad transformers, but in a way that feels natural and idiomatic for Scala.

One Monad to Rule Them All

  1. 1. One Monad to Rule Them All Functional JVM Meetup Prague, Aug 8 2019 John A. De Goes — @jdegoes
  2. 2. Agenda 1. INTRO TO FUNCTIONAL EFFECTS 2 2. TOUR OF THE EFFECT ZOO 3. VERTICAL EFFECT COMPOSITION 4. INTRO TO EFFECT ROTATION 5. PRACTICE OF EFFECT ROTATION
  3. 3. 1. INTRO TO FUNCTIONAL EFFECTS 3
  4. 4. 1. Intro to Functional Effects 4 World of Values Variables Remove Duplication In a Single Expression Functions Remove Duplication Across Multple Expressions Combinators Remove Higher-Order Duplication Across Expressions Testing Test Values for Equality, Similarity, Inequality
  5. 5. 1. Intro to Functional Effects 5 World of Values Variables Remove Duplication In a Single Expression Functions Remove Duplication Across Multple Expressions Combinators Remove Higher-Order Duplication Across Expressions Testing Test Values for Equality, Similarity, Inequality
  6. 6. 1. Intro to Functional Effects 6 World of Values Variables Remove Duplication In a Single Expression Functions Remove Duplication Across Multple Expressions Combinators Remove Higher-Order Duplication Across Expressions Testing Test Values for Equality, Similarity, Inequality
  7. 7. 1. Intro to Functional Effects 7 World of Values Variables Remove Duplication In a Single Expression Functions Remove Duplication Across Multple Expressions Combinators Remove Higher-Order Duplication Across Expressions Testing Test Values for Equality, Similarity, Inequality
  8. 8. 1. Intro to Functional Effects 8 World of Values Variables Remove Duplication In a Single Expression Functions Remove Duplication Across Multple Expressions Combinators Remove Higher-Order Duplication Across Expressions Testing Test Values for Equality, Similarity, Inequality Cases Packages Types Fields Input/Output Methods
  9. 9. 1. Intro to Functional Effects 9 ● GO RUNNING
  10. 10. 1. Intro to Functional Effects def monitor: Boolean = { if (sensor.tripped) { securityCompany.call() true } else false } 10
  11. 11. 1. Intro to Functional Effects 11 sealed trait Alarm[+A] case class Return [A](v : A) extends Alarm[A] case class CheckTripped[A](f : Boolean => Alarm[A]) extends Alarm[A] case class Call [A](next: Alarm[A]) extends Alarm[A] val check: Alarm[Boolean] = CheckTripped(tripped => if (tripped) Call(Return(true)) else Return(false))
  12. 12. 1. Intro to Functional Effects 12 ● GO RUNNING Execution (Interpretation)
  13. 13. 1. Intro to Functional Effects 13 def interpret[A](alarm: Alarm[A]): A = alarm match { case Return(v) => v case CheckTripped(f) => interpret(f(sensor.tripped)) case Call(next) => securityCompany.call(); interpret(next) }
  14. 14. 1. Intro to Functional Effects 14 "A functional effect is an immutable data type equipped with a set of core operations that together provide a complete, type-safe model of a domain concern." — John A. De Goes
  15. 15. 1. Intro to Functional Effects 15 For every concern, there is a functional effect Concern Effect Execution Optionality Option[A] null or A Disjunction Either[A, B] A or B Nondeterminism List[A] Option[A] Input/Output IO[A] throw or A
  16. 16. 16 Optionality (The Painful Way) 1. Intro to Functional Effects
  17. 17. 17 A functional effect for optionality Maybe[A] Succeeds with values of type A 1. Intro to Functional Effects
  18. 18. 18 A functional effect for optionality Operation Signature Present A => Maybe[A] Absent Maybe[Nothing] Map (Maybe[A], A => B) => Maybe[B] Chain (Maybe[A], A => Maybe[B]) => Maybe[B] 1. Intro to Functional Effects
  19. 19. 19 sealed trait Maybe[+A] case class Present[A](value: A) extends Maybe[A] case object Absent extends Maybe[Nothing] case class Map[A, B](maybe: Maybe[A], mapper: A => B) extends Maybe[B] case class Chain[A, B](first: Maybe[A], callback: A => Maybe[B]) extends Maybe[B] A functional effect for optionality 1. Intro to Functional Effects
  20. 20. 20 sealed trait Maybe[+A] { self => def map[B](f: A => B): Maybe[B] = Map(self, f) def flatMap[B](f: A => Maybe[B]): Maybe[B] = Chain(self, f) } object Maybe { def present[A](value: A): Maybe[A] = Present[A] val absent: Maybe[Nothing] = Absent } A functional effect for optionality 1. Intro to Functional Effects
  21. 21. def interpret[Z, A](ifAbsent: Z, f: A => Z)(maybe: Maybe[A]): Z = maybe match { case Present(a) => f(a) case Absent => ifAbsent case Map(old, f0) => interpret(ifAbsent, f.compose(f0))(old) case Chain(old, f) => interpret(ifAbsent, a => interpret(ifAbsent, f)(f(a)))(old) } 21 A functional effect for optionality 1. Intro to Functional Effects
  22. 22. 22 Core operations for functional effects Operation Signature Functional Type Class pure / point A => F[A] Applicative empty / zero F[Nothing] MonadPlus map (F[A], A => B) => F[B] Functor flatMap (F[A], A => F[B]) => F[B] Monad zip / ap [F[A], F[B]) => F[(A, B)] Apply 1. Intro to Functional Effects
  23. 23. 23 For comprehension syntax for monadic effects 1. Intro to Functional Effects for { user <- lookupUser(userId) profile <- user.profile pic <- profile.picUrl } yield pic lookupUser(userId).flatMap(user => user.profile.flatMap(profile => profile.picUrl.map(pic => pic)))
  24. 24. 2. TOUR OF THE EFFECT ZOO 24
  25. 25. 25 The functional effect of optionality 2. Tour of the Effect Zoo sealed trait Option[+A] final case class Some[+A](value: A) extends Option[A] case object None extends Option[Nothing] Option[A]
  26. 26. 26 The functional effect of optionality 2. Tour of the Effect Zoo // Core operations: def some[A](v: A): Option[A] = Some(a) val none: Option[Nothing] = None def map[A, B](o: Option[A], f: A => B): Option[B] def flatMap[B, B](o: Option[A], f: A => Option[B]): Option[B] // Execution / Interpretation: def fold[Z](z: Z)(f: A => Z)(o: Option[A]): Z Option[A]
  27. 27. 27 The functional effect of optionality 2. Tour of the Effect Zoo for { user <- lookupUser(userId) profile <- user.profile pic <- profile.picUrl } yield pic Option[A]
  28. 28. 28 The functional effect of failure 2. Tour of the Effect Zoo sealed trait Either[+E, +A] final case class Left[+E](value: E) extends Either[E, Nothing] case class Right[+A](value: A) extends Eitherr[Nothing, A] Either[E, A]
  29. 29. 29 The functional effect of failure 2. Tour of the Effect Zoo Either[E, A] // Core operations: def left[E](e: E): Either[E, Nothing] = Left(e) def right[A](a: A): Either[Nothing, A] = Right(a) def map[E, A, B](o: Either[E, A], f: A => B): Either[E, B] def flatMap[E, A, B](o: Either[E, A], f: A => Either[E, B]): Either[E, B] // Execution / Interpretation: def fold[Z, E, A](left: E => Z, right: A => Z)(e: Either[E, A]): Z
  30. 30. 30 The functional effect of failure 2. Tour of the Effect Zoo for { user <- decodeUser(json1) profile <- decodeProfile(json2) pic <- decodeImage(profile.encPic) } yield (user, profile, pic) Either[E, A]
  31. 31. 31 The functional effect of logging 2. Tour of the Effect Zoo final case class Writer[+W, +A](run: (Vector[W], A)) Writer[W, A]
  32. 32. 32 The functional effect of logging 2. Tour of the Effect Zoo Writer[W, A] // Core operations: def pure[A](a: A): Writer[Nothing, A] = Writer((Vector(), a)) def write[W](w: W): Writer[W, Unit] = Writer((Vector(w), ())) def map[W, A, B](o: Writer[W, A], f: A => B): Writer[W, B] def flatMap[W, A, B](o: Writerr[W, A], f: A => Writer[W, B]): Writer[W, B] // Execution / Interpretation: def run[W, A](writer: Writer[W, A]): (Vector[W], A)
  33. 33. 33 The functional effect of logging 2. Tour of the Effect Zoo for { user <- pure(findUser()) _ <- log(s"Got user: $user") _ <- pure(getProfile(user)) _ <- log(s"Got profile: $profile") } yield user Writer[W, A]
  34. 34. 34 The functional effect of state 2. Tour of the Effect Zoo final case class State[S, +A](run: S => (S, A)) State[S, A]
  35. 35. 35 The functional effect of state 2. Tour of the Effect Zoo State[S, A] // Core operations: def pure[S, A](a: A): State[S, A] = State[S, A](s => (s, a)) def get[S]: State[S, S] = State[S, S](s => (s, s)) def set[S](s: S): State[S, Unit] = State[S, S](_ => (s, ())) def map[S, A, B](o: State[S, A], f: A => B): State[S, B] def flatMap[S, A, B](o: State[S, A], f: A => State[S, B]): State[S, B] // Execution / Interpretation: def run[S, A](s: S, state: State[S, A]): (S, A)
  36. 36. 36 The functional effect of state 2. Tour of the Effect Zoo for { _ <- set(0) v <- get _ <- set(v + 1) v <- get } yield v State[S, A]
  37. 37. 37 The functional effect of reader 2. Tour of the Effect Zoo final case class Reader[-R, +A](run: R => A) Reader[R, A]
  38. 38. 38 The functional effect of reader 2. Tour of the Effect Zoo Reader[R, A] // Core operations: def pure[A](a: A): Reader[Any, A] = Reader[Any, A](_ => a) def environment: Reader[R, R] = Reader[R, R](r => r) def map[R, A, B](r: Reader[R, A], f: A => B): Reader[R, B] def flatMap[R, A, B](r: Reader[R, A], f: A => Reader[R, B]): Reader[R, B] // Execution / Interpretation: def provide[R, A](r: R, reader: Reader[R, A]): A
  39. 39. 39 The functional effect of reader 2. Tour of the Effect Zoo for { port <- environment[Config].map(_.port) server <- environment[Config].map(_.server) retries <- environment[Config].map(_.retries) } yield (port, server, retries) Reader[R, A]
  40. 40. 40 The functional effect of asynchronous input/output 2. Tour of the Effect Zoo final case class IO[+A](unsafeRun: (Try[A] => Unit) => Unit) IO[A]
  41. 41. 41 The functional effect of asynchronous input/output 2. Tour of the Effect Zoo // Core operations: def sync[A](v: => A): IO[A] = IO(_(Success(v))) def async[A](r: (Try[A] => Unit) => Unit): IO[A] = IO(r) def fail(t: Throwable): IO[Nothing] = IO(_(Failure(t))) def map[A, B](o: IO[A], f: A => B): IO[B] def flatMap[B, B](o: IO[A], f: A => IO[B]): IO[B] // Execution / Interpretation: def unsafeRun[A](io: IO[A], k: Try[A] => Unit): Unit IO[A]
  42. 42. 3. VERTICAL EFFECT COMPOSITION 42
  43. 43. 43 Option + Either 3. Vertical Effect Composition
  44. 44. 44 3. Vertical Effect Composition final case class OptionEither[+E, +A](run: Either[E, Option[A]]) { def map[B](f: A => B): OptionEither[E, B] = ??? def flatMap[B](f: A => OptionEither[E, B]): OptionEither[E, B] = ??? } object OptionEither { def pure[A](a: A): OptionEither[Nothing, A] = lift(Some(a)) def lift[A](option: Option[A]): OptionEither[Nothing, A] = OptionEither(Right(option)) } Vertical composition of Option atop Either
  45. 45. 45 3. Vertical Effect Composition final case class OptionT[F[_], +A](run: F[Option[A]]) { def map[B](f: A => B)(implicit F: Functor[F]): OptionT[F, B] = ??? def flatMap[B](f: A => OptionT[F, B])(implicit F: Monad[F]): OptionT[F, B] = ??? } object OptionT { def pure[A](a: A)(implicit F: Applicative[F]): OptionT[F, A] = lift(Some(a)) def lift[A](option: Option[A])(implicit F: Applicative[F]): OptionT[F, A] = OptionT(F.pure(option)) } Vertical composition of Option atop F[_]
  46. 46. 46 3. Vertical Effect Composition def myCode[F[_]: Monad]: F[Result] = { // Introduce state effect locally: def inner: StateT[F, MyState, Result] = ??? // Eliminate state effect locally: inner.run(MyState.Initial) } Monad transformers allow local effect introduction / elimination
  47. 47. 47 3. Vertical Effect Composition "Monad transformers allow modular composition of separate functional effect types into a single functional effect, with the ability to locally introduce and eliminate effect types." — John A. De Goes
  48. 48. 48 Option + Either + Writer + State + Reader 3. Vertical Effect Composition
  49. 49. 49 type GodMonad[+E, +W, S, -R, A] = OptionT[ EitherT[ WriterT[ StateT[ Reader[R, ?], S, ?], W, ?], E, ?], ?] 3. Vertical Effect Composition
  50. 50. 50 3. Vertical Effect Composition val effect: GodMonad[Throwable, String, Int, Config, Int] = OptionT( EitherT( WriterT( StateT( Reader(_ => 42))))) Monad transformers are extremely cumbersome to use directly!
  51. 51. 51 3. Vertical Effect Composition Monad transformers cripple performance even with opaque types! F OptionT[F] StateT[EitherT[OptionT[F, ?], E, ?], S, ?] ...
  52. 52. 52 3. Vertical Effect Composition StateT[EitherT[F, E, ?], S, ?] != EitherT[StateT[F, S, ?], E, ?] Monad transformers are extremely order-sensitive! n layers = n! possible orderings
  53. 53. 53 3. Vertical Effect Composition def myCode[F[_]: Monad]: F[Result] = { // Cannot simply introduce / eliminate // monad state for deeper levels!!! def inner[G[_]: Monad](implicit G: MonadState[G, MyState, ?]): G[Result] = ??? inner[F] // Will not compile! } Tagless-final obliterates local effect introduction / elimination!
  54. 54. 54 3. Vertical Effect Composition MonadError[StateT[EitherT[IO, MyError, ?], MyState, ?], MyError, ?] Due to encoding + compiler, transformers infer poorly!
  55. 55. 55 3. Vertical Effect Composition Monad transformerr checklist Monad Transformers Intro / Elimination ✅ Type-Inference ❌ Order-Insensitive ❌ Encapsulated ✅ Ergonomic ❌
  56. 56. 4. INTRO TO EFFECT ROTATION 56
  57. 57. 57 4. Intro to Effect Rotation sealed trait Either[+A, +B] case class Left [A](value: A) extends Either[A, Nothing] case class Right[B](value: B) extends Either[Nothing, B] Either is the simplest possible failure + success effect
  58. 58. 58 4. Intro to Effect Rotation // Introduction: def fail[E](e: E): Either[E, Nothing] // Elimination: def catchAll[E, A](et: Either[E, A])(f: E => A): Either[Nothing, A] Either is the simplest possible failure + success effect Compile-time proof of elimination
  59. 59. 59 4. Intro to Effect Rotation final case class REither[-R, +E, +A](run: R => Either[E, A]) Why stop with error effects?
  60. 60. 60 4. Intro to Effect Rotation // Introduction: def environment[R]: REither[R, Nothing, R] = REither(r => Right(r)) // Elimination: def provide[R, E, A](r: R)(re: REither[R, E, A]): REither[Any, E, A] = REither[Any, E, A](_ => re.run(re)) Introduction and elimination for the reader effect Compile-time proof of elimination
  61. 61. 61 4. Intro to Effect Rotation final case class REither[-R, +E, +A](run: R => Either[E, A]) Naive encoding shares some overhead with transformers Dispatch + Boxing Overhead
  62. 62. 62 4. Intro to Effect Rotation
  63. 63. 63 4. Intro to Effect Rotation MTL[R, W, S, E, A]
  64. 64. 64 4. Intro to Effect Rotation sealed trait MTL[-R, +W, S, +E, +A] { self => def map[B](f: A => B): MTL[R, W, S, E, B] = ??? def flatMap[..](f: A => MTL[R1, W1, S, E1, B]): MTL[R1, W1, S, E1, B] = ??? // Optimized interpreter for GADT: def run(r: R, s: S): (List[W], S, Either[E, A]) = ??? } Possible to eliminate overhead with "free" encodings
  65. 65. 65 4. Intro to Effect Rotation object MTL { final case class Succeed[S, +A](value: A) extends MTL[Any, Nothing, S, Nothing, A] final case class Reader [R, S]() extends MTL[R, Nothing, S, Nothing, R] final case class RunReader[R, +W, S, +E, +A](r: R, mtl: MTL[R, W, S, E, A]) extends MTL[Any, W, S, E, A] final case class Error [S, +E](error: E) extends MTL[Any, Nothing, S, E, Nothing] final case class RunError[-R, +W, S, +E, +A](mtl: MTL[R, W, S, E, A]) extends MTL[Any, W, S, Nothing, Either[E, A]] final case class State [S, +A](f: S => (S, A)) extends MTL[Any, Nothing, S, Nothing, A] final case class RunState[-R, +W, S, +E, +A](s: S, mtl: MTL[R, W, S, E, A]) extends MTL[R, W, Unit, E, A] final case class Writer [+W, S](w: W) extends MTL[Any, W, S, Nothing, Unit] final case class RunWriter[-R, +W, S, +E, +A](mtl: MTL[R, W, S, E, A]) extends MTL[R, Nothing, S, E, (List[W], A)] final case class FlatMap[R, W, S, E, A, R1 <: R, W1 >: W, E1 >: E, B](first: MTL[R, W, S, E, A], k: A => MTL[R1, W1, S, E1, B]) extends MTL[R1, W1, S, E1, B] } Possible to eliminate overhead with "free" encodings
  66. 66. 66 4. Intro to Effect Rotation Elimination laws for effect rotation Effect Type Type Elimination Examples Covariant Nothing Failure, Optionality, Writer Contravariant Any Reader Invariant Unit State
  67. 67. 67 4. Intro to Effect Rotation Monad transformers versus effect rotation Monad Transformers Effect Rotation Intro / Elimination ✅ ✅ Type-Inference ❌ ✅ Order-Insensitive ❌ ✅ Encapsulated ✅ ❌ Ergonomic ❌ ✅
  68. 68. 5. PRACTICE OF EFFECT ROTATION 68
  69. 69. 69 5. Practice of Effect Rotation ZIO[R, E, A] Reader Error Success
  70. 70. 70 5. Practice of Effect Rotation Reader trait ZIO[R, E, A] { def provide(r: R): ZIO[Any, E, A] = ??? } object ZIO { def environment: ZIO[R, Nothing, R] = ??? def accessM[R, E, A](f: R => ZIO[R, E, A]): ZIO[R, E, A] = ??? }
  71. 71. 71 5. Practice of Effect Rotation Error trait ZIO[R, E, A] { def either: ZIO[R, Nothing, Either[E, A]] = ??? } object ZIO { def fail(e: E): ZIO[Any, E, Nothing] = ??? def succeed(a: A): ZIO[Any, Nothing, A] = ??? }
  72. 72. 72 5. Practice of Effect Rotation Option via Error ZIO[R, Unit, A]
  73. 73. 73 5. Practice of Effect Rotation Option + Either via Error ZIO[R, Option[E], A]
  74. 74. 74 5. Practice of Effect Rotation trait Ref[A] { def update(f: A => A): UIO[A] def modify[B](f: A => (B, A)): UIO[B] def get: UIO[A] def set(a: A): UIO[Unit] } Reader + Ref / TRef is the key to unlocking other effects!
  75. 75. 75 5. Practice of Effect Rotation Writer via Reader trait Writer[W] { def writer: Ref[Vector[W]] } def write[W](w: W): ZIO[Writer[W], Nothing, Unit] = ZIO.accessM[Writer[W]](_.writer.update(_ :+ w).unit)
  76. 76. 76 5. Practice of Effect Rotation State via Reader trait State[S] { def state: Ref[S] } def modify[S, A](f: S => (S, A)): ZIO[State[S], Nothing, A] = ZIO.accessM[State[S]](_.state.modify(f)) def update[S, A](f: S => S): ZIO[State[S], Nothing, Unit] = modify(s => (s, ())) def gets[S]: ZIO[State[S], Nothing, S] = modify(s => (s, s))
  77. 77. 77 5. Practice of Effect Rotation God Monad type Fx[S, W] = State[S] with Writer[W] type GodMonad2[+E, W, S, -R <: Fx[S, W], A] = ZIO[R, Option[E], A]
  78. 78. 78 5. Practice of Effect Rotation Effect rotation delivers resource / concurrent safety! Monad Transformers Effect Rotation ... ... ... Concurrent-Safe State ❌ ✅ Concurrrent Safe Writer ❌ ✅ Resource-Safe State ❌ ✅ Resource-Safe Writer ❌ ✅
  79. 79. THANK YOU! Any questions? You should follow me on Twitter: @jdegoes And bookmark my blog: http://degoes.net 79

×