January 27, 2017
Motivated by the post of /u/saosebastiao I will be covering an example of how to use Eff as a replacement for monad transformers, I will be using Cats for our monad transformers, and Eff for the Eff monad.
I will use the following as our running example for the different approaches:
We will create a method that gets the state of an Int variable. If it is larger than 0 we will decrease it by 1, otherwise we will throw an error.
We can identify that we have two types of effects: a State-effect (accessing and mutating a mutable variable) and an Error-effect (exiting computation with an error).
Let’s first take a look at the monad transformer approach. In this approach we work with the actual monad transformers to define our method. Let’s try that:
(Note that the ?
alternate syntax for type lambdas comes from Kind Projector)
type MonadStackStateTEither[ErrorType, StateType, ReturnType] =
StateT[Either[ErrorType, ?], StateType, ReturnType]
def decrMonadStackStateTEither: MonadStackStateTEither[String, Int, Unit] = ???
We can see that we compose the Either
monad (which is what we’ll be using as our Error-effect during this post) with the State
monad by using the StateT
monad transformer.
This gives us a monad which can do both Error-effects and State-effects, which I called MonadStackStateTEither
. Let’s complete our method definition:
def decrMonadStackStateTEither: MonadStackStateTEither[String, Int, Unit] = for {
x <- StateT.get[Either[String, ?], Int]
_ <- if (x > 0) { StateT.set[Either[String, ?], Int](x - 1) }
else { StateT.lift[Either[String, ?], Int, Unit](Left("error")) }
} yield ()
We can call the get and set methods on the StateT
object to get correctly typed State-effects. If we want to use an Error-effect though, we have to use StateT.lift
to lift an effect from the Error-effect to our composed stack of State and Error-effects. The lifting makes the types match up so that the Error-effect correctly matches the MonadStackStateTEither
type.
You might have noticed that we arbitrarily chose to use StateT
on top of Either
instead of the other way around. It might look like a small detail, but it is in fact something that can completely change the computed value. So the order in which the monad transformers are set up is important.
We can try the other way around as well. We will use the EitherT
monad transformer on the State
monad to receive a monad which can do Error and State-effects.
type MonadStackEitherTState[ErrorType, StateType, ReturnType] =
EitherT[State[StateType, ?], ErrorType, ReturnType]
def decrMonadStackEitherTState: MonadStackEitherTState[String, Int, Unit] = for {
x <- EitherT.liftT[State[Int, ?], String, Int](State.get[Int])
_ <- if (x > 0) { EitherT.liftT[State[Int, ?], String, Unit](State.set(x - 1)) }
else { EitherT.left[State[Int, ?], String, Unit](State.pure("error")) }
} yield ()
We see that now we have to lift State-effects into the correct type via the EitherT.liftT
method.
If we run our method we can see that the results we get have different types, which is part of the reason why the order of the monad transformers matter.
(Note that in our second example we need the first .value
to access the .run
method from State
and the second .value
because State[S, A]
is an alias for StateT[Eval, S, A]
, which means that we have an Eval
effect as well)
val resultStateTEither: Either[String, (Int, Unit)] =
decrMonadStackStateTEither.run(0)
// returns: Left(error)
val resultEitherTState: (Int, Either[String, Unit]) =
decrMonadStackEitherTState.value.run(0).value
// returns: (0,Left(error))
In this style we have typeclasses for our effects. In our case we will be using MonadState
and MonadError
. These typeclasses define methods which make sense for their corresponding effects. So MonadState
defines methods to interact with a State-effect (get, put, modify, …) and MonadError
defines methods to interact with an Error-effect (raise, catch, …).
We declare the result type of our method to be a generic F[_]
. Then we implicitly take a MonadState[F]
and MonadError[F]
. This means that this method is generic for any F[_]
which can interact as both of those effects. In this way we do not have to lift any effects into our result type, this lifting will be done in the typeclass instances if necessary.
// beware: does NOT compile, see below
def decrMtlStateError[F[_]](implicit
ms: MonadState[F, Int],
me: MonadError[F, String]): F[Unit] = for {
x <- ms.get
_ <- if (x > 0) { ms.set(x - 1) }
else { me.raiseError("error") }
} yield ()
This method unfortunately does not compile (on Scala 2.12.1 and Cats 0.9.0). The reason for that is that MonadState
and MonadError
both are Monad[F]
instances and so there is an ambiguous implicit. We can resolve this by explicitly using the flatMap
method on one of both.
We can write it like this to get it compiled, but it looks a bit ugly:
def decrMtlStateError[F[_]](implicit
ms: MonadState[F, Int],
me: MonadError[F, String]): F[Unit] = {
ms.flatMap(ms.get) { x =>
if (x > 0) { ms.set(x - 1) }
else { me.raiseError("error") }
}
}
Then we can reuse this method for both monad types we used earlier in our example… If there would be a MonadState
instance defined for EitherT
, which as of writing this post is not the case (on Cats 0.9.0). But we can define our own for now (*).
val resultMtlStateTEither: Either[String, (Int, Unit)] =
decrMtlStateError[StateT[Either[String, ?], Int, ?]].run(0)
// returns: Left(error)
val resultMtlEitherTState: (Int, Either[String, Unit]) =
decrMtlStateError[EitherT[State[Int, ?], String, ?]].value.run(0).value
// returns: (0,Left(error))
We see we get the same results as in the previous paragraph, but we could reuse our generic method.
I’m not sure if this style is common in Scala, since playing with this simple example I already came across some issues. But I thought it would be interesting to include it, since it might provide some additional insight moving from monad transformers to Eff.
The way we use effects in Eff is somewhat similar to the mtl-style. We also have to declare as implicit parameters which effects we want to use. Let’s define some aliases for our effects (like in the Eff user guide):
type _eitherString[R] = Either[String, ?] |= R
type _stateInt[R] = State[Int, ?] |= R
In Eff the type variable R
signifies the effect stack. This stack will contain all the effects which our methods can utilize. The |=
helper functions means that the effect on the left is a member of the stack R
. We will use this as an implicit parameter in the same way as we took MonadState
and MonadError
as implicit parameters.
Our method signature looks like this:
def decrEff[R : _eitherString : _stateInt]: Eff[R, Unit] = ???
Our effect stack R
is a generic type and it must contain at least our _eitherString
and _stateInt
effect . Our result is Eff[R, A]
, which means that we will get a result of type A
, but we might also execute effects according to the effects in the stack R
. For our method A
is Unit
.
By importing the Eff syntax (import org.atnos.eff._, org.atnos.eff.all._, org.atnos.eff.syntax.all._) we can get a method which looks very similar to our mtl-style method:
def decrEff[R : _eitherString : _stateInt]: Eff[R, Unit] = for {
x <- get
_ <- if (x > 0) { put(x - 1) }
else { left("error") }
} yield ()
Now when calling our method we have to do two things: 1. Choose which effect types we want to use to handle the effects in our stack 2. Choose in which order we will run our effects.
Choosing the types can be done like this:
type FxStack = Fx.fx2[State[Int, ?], Either[String, ?]]
And we can choose the order of effects by composing the runEither
and runState
methods differently.
val resultRunStateRunEither: Either[String, (Unit, Int)] =
decrEff[FxStack].runState(0).runEither.run
// returns: Left(error)
val resultRunEitherRunState: (Either[String, Unit], Int) =
decrEff[FxStack].runEither.runState(0).run
// returns: (Left(error),0)
So hopefully by going through this example the basics of Eff are explained well enough to go through the user guide and learn more about it.
You can find all the code used in this post on this gist.
The MonadState
instance for EitherT
I used is:
implicit def monadStateEitherT[F[_], E, S](implicit ms: MonadState[F, S])
: MonadState[EitherT[F, E, ?], S] = new MonadState[EitherT[F, E, ?], S] {
val F = Monad[F]
override def get: EitherT[F, E, S] = EitherT.liftT(ms.get)
override def set(s: S): EitherT[F, E, Unit] = EitherT.liftT(ms.set(s))
// copied from cats EitherTMonad
override def pure[A](a: A): EitherT[F, E, A] = EitherT(F.pure(Either.right(a)))
override def flatMap[A, B](fa: EitherT[F, E, A])(f: A => EitherT[F, E, B]): EitherT[F, E, B] = fa flatMap f
override def tailRecM[A, B](a: A)(f: A => EitherT[F, E, Either[A, B]]): EitherT[F, E, B] =
EitherT(F.tailRecM(a)(a0 => F.map(f(a0).value) {
case Left(l) => Right(Left(l))
case Right(Left(a1)) => Left(a1)
case Right(Right(b)) => Right(Right(b))
}))
}