A general monad for managing stacking effects
Otherwise known as "less confusing monad transformers".
Warning The monad produced by
Emmis not guaranteed to be a lawful monad! In fact, it isn't even guaranteed to be a lawful applicative. You can see an example of a violation of the applicative laws here. (much thanks to @TomasMikula!) I'm leaving this repository up for pedagogical reasons; it's still an interesting exploration of Scala's type system. But I do not recommend you use it for real work, given that the monad is not lawful.
The
Emmmonad provides a syntactically lightweight, type-inference friendly data type for composing effects. The general motivation is very similar to monad transformers, but the end result is far more user friendly and also significantly more general. The main goals of the project are as follows:
These goals are very similar to those which motivated Oleg's
Eff, which is a really terrific data structure. There are some significant differences though. Most notably,
Effrequires effect implementations to be rewritten to be compatible with its internal calculus, and so it does not allow the composition of arbitrary "standalone" monads written in a conventional style. However,
Effis able to provide much greater expressive power than
Emm(or monad transformers) in several key diminsions. Oleg goes into significant detail on the expressiveness gains of
Effin his paper describing the construct.
Emmdoes not provide the same benefits.
If you want to use
Emmin your project, adding the following SBT configuration will do the trick:
libraryDependencies += "com.codecommit" %% "emm-core" % EmmVersion
You will also need to bring in the appropriate upstream framework support for either Scalaz or Cats, depending on which one you're using.
libraryDependencies += "com.codecommit" %% "emm-scalaz-71" % EmmVersion // for scalaz 7.1// or!
libraryDependencies += "com.codecommit" %% "emm-scalaz-72" % EmmVersion // for scalaz 7.2
// or!
libraryDependencies += "com.codecommit" %% "emm-cats" % EmmVersion // for cats 0.4.1
You will want to use either
emm-scalazor
emm-cats. While there is no technical reason you would not be able to use both in the same project, doing so would be… weird. At present, Cats support is slightly more complete than Scalaz, but we aim to reach parity soon.
The most recent stable version of Emm is 0.2.1.
val EmmVersion = "0.2.1"
Snapshot builds are often published as versions derived from the git hash. For example,
0.2-a21c63a. The version prefix indicates compatibility with a particular version line, not derivation or antecedence. Not all git hashes are published, but some are. When in doubt, try a few. Or just ask for one to be published. All artifacts are signed with public key fingerprint 2BAE 5960.
import emm._ import emm.compat.scalaz._import scalaz.concurrent.Task import scalaz.std.option._
def readName: Task[String] = ??? def log(msg: String): Task[Unit] = ???
type E = Task |: Option |: Base
val effect: Emm[E, String] = for { first
The above is analogous to monad transformers in many ways. In fact, we can write the exact same code from above using
OptionT:import scalaz._ import scalaz.concurrent.Task import scalaz.syntax.monad._def readName: Task[String] = ??? def log(msg: String): Task[Unit] = ???
val effect: OptionT[Task, String] = for { first
The advantages of
Emmbecome much more apparent when attempting to stack more than just two monads simultaneously. For example, one might imagine stackingTask,Optionand right-biasedEither. Let's enrich our previous example with some error handling (note that I'm using kind projector to avoid type lambdas):import emm._ import emm.compat.scalaz._import scalaz._ import scalaz.concurrent.Task import scalaz.std.option._
def readName: Task[String] = ??? def log(msg: String): Task[Unit] = ???
type E = Task |: (String / ?) |: Option |: Base
val effect: Emm[E, String] = for { first
It works as expected, with all the same syntax as before. However, if we look at the same example using monad transformers, a rather distopian picture emerges:
import scalaz._ import scalaz.concurrent.Task import scalaz.syntax.monad._def readName: Task[String] = ??? def log(msg: String): Task[Unit] = ???
val effect: OptionT[EitherT[Task, String, ?], String] = for { first
That's a lot of very explicit lifting and special syntax. I had to ponder quite long and hard about the above, and I'm not even sure if I got it all right! Monad transformers are very ugly, very cumbersome, and when you get things wrong they explode in remarkably spectacular ways.
The
Emmmonad is intended to change all of that. It is intended to be very straightforward to manage and extend complex stacks of effects, and to do so without any special wrappers or added complexity from the effect author. No need to write anOptionT, just useOption!API
The following API is provided. For starters, the following pair of functions are implicitly provided to lift values into the effect stack:
pointM[C <: effects> – Points a value of typeAinto the monad,Emm[C, A]. Requires anApplicativefor each component of the effect stackC.
liftM[C <: effects> – Given an effect which is of a type contained withinC, lift the effect into the full effect stack represented byC. For example:Option(42).liftM[Task |: Option |: Base]
wrapM[C <: effects> – Given a full stack of effects which matches the stackC, wrap the stack in theEmmmonad. Note that theCparameter can be inferred basically 100% of the time, but can be provided explicitly to assert correctness. Example:(Task now Option(42)).wrapM. This is equivalent to calling theEmm(...)constructor, but the type inference is much nicer.
These methods are exposed via implicit classes contained within the
emmpackage object. All of the above methods are aliased on the
Emmobject as
point,
liftand
wrap, respectively. You'll notice, however, that they do require a bit of extra type annotation since the target type and the effect stack are in the same type block, rather than separate ones (as in the case of implicitly provided members). Thus, you should generally prefer the "
Mversions" of each method wherever possible (i.e. when not importing
scalaz.syntax.monad._).
The
Emmmonad itself provides the following (effective) API:
map[B](A => B): Emm[C, B]– Conventional functor map. Transforms the value within the effect
flatMap[B](A => Emm[C, B]): Emm[C, B]– Monadic bind. Transforms the value within the effect and joins the two effect stacks. This function requires that all components of
Cdefine a
bindfunction, and all components aside from the outer-most (left-most) must have a
Traverseinstance.
flatMapM[G[_], B](A => G[B]): Emm[C, B]– Similar to
flatMap, except instead of transforming the value to an effect contained within the entire effect stack,
C, it transforms the value to a single component of that effect stack. Thus,
Gmust be in
C. The result is joined with the effect stack and returned within
Emm.
expand– The inverse of
collapse. Converts an
Emmof the form
Emm[... |: F |: Base, A]into
Emm[... |: Base, F[A]]. This is extremely useful when there are effect-specific functions (e.g.
Option#getOrElse) that you need to access on the inner-most (right-most) effect of the stack. Once you have expanded, you can use
mapor
flatMapto access these functions and manipulate the inner-most effect. Runs in constant time.
collapse– The inverse of
expand. Converts an
Emmof the form
Emm[... |: Base, F[A]]into
Emm[... |: F |: Base, A]. This is generally most useful in conjunction with
expand, where you have manipulated the inner-most effect and you need to "recombine" the results of that manipulation with the full effect stack. Runs in constant time.
run– Unwraps the effect stack (without modification) from
Emm. Effectively, this takes a type of the form
Emm[F |: G |: Base, A]and produces a type of the form
F[G[A]]. Literally, it is the "contents" of
Emm.
Right now, this is sitting on top of the shims 0.2 typeclass hierarchy, which is to say that it supports Cats 0.3, Scalaz 7.2 and 7.1. Everything is implemented in terms of the following type classes (with minimal constraints for every function):
Applicative
FlatMap
Functor
Traverse
Constraints which are not required to evaluate a given function are not assumed. For example, consider the following effect stack:
type E = Option |: Task |: Baseval effect = Option(42).liftM[E]
If you attempt to run
flatMapon this effect stack, you will run into problems:
effect flatMapM { i => if (i < 20) None else Some(i * 2) } // does not compile!
This will fail because
Taskis not the outer-most effect, which is to say, it isn't the effect on the far left of the effect definition. The reason this is a problem becomes more clear if we look at things in terms of
map,
flattenand the raw stack, rather than simply
flatMapand the collapsed
Emmmonad:
val effect2: Option[Task[Int]] = Some(Task now 42)val mapped: Option[Task[Option[Task[Int]]]] = effect2 map { t => t map { i => if (i < 20) None else Some(Task now (i * 2)) } }
val result: Option[Task[Int]] = mapped.flatten // ??????
Notice the problem here. We need to take the second
Optionlayer, which is within a
Task, and "flip" it outside of the
Tasklayer in order to flatten the
Optionand
Tasklayers together. Basically, we want to do something like this:
Option[Task[Option[Task[Int]]]] => Option[Option[Task[Task[Int]]]] => Option[Task[Task[Int]]] => Option[Task[Int]]
Clearly, there are no problems with the last two stages, but that second stage is completely impossible. We can't take a value from inside
Taskand "flip" it to the outside.
Taskis basically a
Future, so the value "inside" of
Taskdoesn't even exist yet! So this effect stack is non-sensical as a monad; we cannot define
flatMap(or equivalently,
flatMapM) on it, and the compiler is very happy to tell us so.
Technically, the reason we can't do this is because there is no instance
Traverse[Task], and in fact you cannot define such an instance without actually running the
Task. Our example from earlier though, where our stack was
Task |: Option |: Basewas just fine, because there is an instance
Traverse[Option].
Here's the cool bit though. Even though it doesn't make any sense to define
flatMapon
Emm[Option |: Task |: Base, Int], there's no reason why we can't define
map!
type E = Option |: Task |: Baseval effect = Option(42).liftM[E]
val effect2 = effect map { _ * 2 } // no problemo!
Even though our effect stack is sort of bogus, it's only bogus if we attempt to treat it as a monad. It's a perfectly valid applicative functor, and we can treat it as such. In other words,
flatMapdoesn't work (and shouldn't work!) on some effect stacks, but
mapworks on any effect stack where each component effect has a
Functor.
Maybe this section should be nearer to the top... Oh well.
The most significant limitation of this approach is caused by everyone's favorite limitation of the scalac type checker, SI-2712. The good news is that this bug is not a complete show stopper; it's relatively easy to work around when you control the entire stack of type signatures (as I do here) and you're not trying to generalize over different type constructor arities. The bad news is that it makes my life very difficult, and it imposes some pretty hard limits (also related to how much boilerplate I'm willing to type out) on what sorts of type constructors do and do not work with
Emm.
Specifically, the following kinds of type constructors are accepted (i.e. will be fully functional in any position of an effect stack):
* -> *– Examples:
Option,
List,
Task
* x * -> *– Examples:
Either,
State(with caveats),
Writer(more caveats),
Reader(sorry, still caveated)
* x * x * -> *– Examples: No idea
(* -> *) x * -> *– Examples:
Free,
OptionT,
ListT,
StreamT
(* -> *) x * x * -> *– Examples: uh...
(* -> *) x * x * x * -> *– Examples:
IndexedStateT(sort of)