Monad transformers and cats
3 OptionT and EitherT tips for beginners
Monad transformers implemented in cats provide rich API allowing great flexibility in the way we shape our applications.
The downside of such powerful API is, that sometimes it might be not an easy task to grasp all the correct uses. It is especially true if you’re new into functional Scala.
So, I thought it would be helpful to share some tips.
My blog post is not meant to explain monad transformers from scratch. I assume you already know basics and if not, you can check this great article.
We will be dealing with arguably the two most popular monad transformers: OptionT and EitherT.
Tip 1. Use the right factory method
The main purpose of monad transformers is altering nested monad stack like IO[Option[A]]
to a single monad data type like OptionT[A]
.
Stack IO[Option[A]]
might describe a computation that performs side-effect returning a single value that might or might not exists. A good example might be a query to the database to get the user by its id:
def getUserById(id: UserId): IO[Option[User]]
We could use OptionT.apply
to transform result returned by the method into OptionT
and then compose it with other methods with similar signature:
The problem is that in real life not all methods in our application will look like this. For example, we could deal with methods performing database queries but always returning a result (we’re ignoring network issues for now):
def getUsers(): IO[List[User]]
Another case is a pure method that takes some value and then optionally returns the result:
def validateUser(user: User): Option[ValidatedUser]
It’s not all. Some methods could return a plain value, another could return stacked IO
and Either
and so on.
Our goal is to compose these various methods with the help of monad transformers. Fortunately, cats library provides multiple convenient factory methods allowing creating OptionT
and EitherT
from various kinds of inputs.
First, let’s check out OptionT
:
- As mentioned above, when dealing with
IO[Option[A]]
it is possible to transform it intoOptionT[IO, A]
withOptionT.apply
. OptionT.liftF
allows lifting anyF[A]
functor into monad transformer.- It’s also possible to create an
OptionT
instance from methods returningOption[A]
withOptionT.fromOption
. - To wrap a pure value into
OptionT
we can useOptionT.some
/OptionT.pure
.OptionT.none
is for creating an emptyOptionT
.
By utilizing these methods we can combine methods with various signatures into IO
program:
EitherT
API provides even more possibilities:
- Similarily to
OptionT
we can transformIO[Either[A, B]
intoEitherT[IO, A, B]
withEitherT.apply
. - We can create
EitherT
fromEither
withEitherT.fromEither
. Moreover, it’s also possible to createEitherT
fromOption
withfromOption
, but in this case, we have to pass an error object as the second parameter, which will be returned asLeft
value in caseOption
isNone
. MethodfromOptionF
works alike, but forIO[Option[A]]
. - To lift functor
F[A]
intoEitherT
you could useliftF[F[_], A, B]
orright[B]
. These methods have the same semantics, but different signatures.right
expects fewer type parameters, which in some cases might help compiler infer correct types. An important remark is that lifting will not handle errors fromIO
, so we suppose to use it only if effect can’t fail with an exception. For methods that might return failedIO
we should useio.attemptT
.There’s more on error handling later. - We can also lift
F[A]
asLeft
withEitherT.left
. It also doesn’t handle errors. EitherT.pure
/EitherT.rightT
are for wrapping a pure value intoEitherT
asRight
andEitherT.leftT
asLeft
.
By using the most fitting methods to instantiate transformers we might save us from writing tons of unnecessary boilerplate.
Tip 2. Help compiler from time to time
Surprisingly the biggest obstacle while working with cats’ monad transformers is very often scala compiler. To be precise scalac can have huge problems to do the right job with type inference.
For instance, it’s very likely that creating monad transformers from effects like Option
or Either
might cause compilation error starting with a very intriguing phrase: diverging implicit expansion.
For instance, you might face this error while using EitherT.fromEither
:
The error message sounds scary, but actually, we can fix it very easily.
Either
has information only about types of success and error channels. When you’re using fromEither
to instantiate EitherT
the compiler very often can’t infer the right type for the F
type parameter. That causes compilation failure because the compiler can’t find proper typeclasses instances.
The solution is very simple: you have to explicitly pass type of F
.
EitherT.fromEither[IO](giveFoodToCat(cat))
The compiler might also have problems with inferring the right types with
functions lifting F
into the transformer (like OptionT.liftF
or EitherT.liftF
). It is especially possible if you tend to omit the return type of functions you write.
In this case, if you’re getting compilation errors, it’s usually a good idea to pass types explicitly.
OptionT.liftF[IO, Throwable, Unit](cat.purr)
The rule of thumb is:
If you’re getting compile errors with monad transformers, try specifying the return type of the composed method and if it’s not enough explicitly specify type parameters of called functions.
Another common problem is related to the left type parameter of Either
or EitherT
.
Let’s say you want to model your business errors as a hierarchy of types extending common type CatError
:
Then let’s create two methods both returning nested IO
and Either
. The error type of first Either
will be CatIsIgnoringYou
and CatIsNotInTheMood
for the second one. Since both error types extend CatError
, you could assume that if you fix the error type of resulting EitherT
to CatError
there would be no compilation errors.
Unluckily, it’s not the case. Left type in Either
and EitherT
in cats effect is invariant and you will get type mismatch:
Again fixing the problem is straightforward. You need to use function leftWiden
which lets you broaden the left type of EitherT
to more general CatError
. It’s usually enough to use leftWiden
on the last function call in flatMap
chain or for-comprehension:
3. Be consistent with error handling
The reason, why we use stack like F[Either[Error, Value]]
, is usually to extend data types like Future
, IO
or Task
from Monix with type-safe encoding for errors via Either
. On the other hand, these effect types can already handle errors on their own.
For example, we could create a failed Future with Future.failed
. Later we could recover from that error using Future.recover
or Future.recoverWith
. With cats, we could use methods provided by MonadError
typeclass: IO.raiseError
to create failed IO
and IO.handleError
or IO.recover
for error handling.
The consequence of this is if we use stack F[Either[Error, Value]]
we have two completely separate channels for propagating errors. Better, no language or framework mechanism stops us from using both at once! This might be a source of confusion and potential mistakes.
Even though we handle the case when Either
is Left
our application will still fail because exception which was raised in bark
method is not intercepted.
You should always use only a single channel for propagating errors at any time. Luckily the cats library provides several convenient methods to switch between these two error channels.
First of all, you can transform F[A]
into F[Either[Throwable,A]
with attempt
. Throwable
will be used as the type of Left
parameter.
It is also possible to narrow the type of error to any subclass of Throwable
with attemptNarrow
.
To directly get EitherT[F, Throwable, A]
you can use attempT
, which is basically equivalent of doing attempt
and ten wrapping result in EitherT
. It might seem that it does the same job as EitherT.liftF
, but lifting doesn’t catch any errors, so it’s supposed to be run only on effects that don’t fail.
Another way around, you can get errors from Either
into IO
with rethrow
or from EitherT
with rethrowT
. This will only work if left type in Either
is a subclass of Throwable
.
An exciting alternative to monad stack F[Either[Value, Error]]
are effect types that already embed type parameter for errors like ZIO or Monix BIO.
Wrapping up
I hope some of the tips provided here will be a good starting point for creating new FP-oriented applications using monad transformers.
Good luck!