Monad transformers and cats

3 OptionT and EitherT tips for beginners

Krzysztof Atłasik
SoftwareMill Tech Blog

--

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 into OptionT[IO, A] with OptionT.apply.
  • OptionT.liftF allows lifting any F[A] functor into monad transformer.
  • It’s also possible to create an OptionT instance from methods returning Option[A] with OptionT.fromOption.
  • To wrap a pure value into OptionT we can use OptionT.some/OptionT.pure. OptionT.none is for creating an empty OptionT.

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 transform IO[Either[A, B] into EitherT[IO, A, B]with EitherT.apply.
  • We can create EitherT from Either with EitherT.fromEither. Moreover, it’s also possible to create EitherT from Option with fromOption, but in this case, we have to pass an error object as the second parameter, which will be returned as Left value in case Option is None. Method fromOptionF works alike, but for IO[Option[A]].
  • To lift functor F[A] into EitherT you could use liftF[F[_], A, B] or right[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 from IO, so we suppose to use it only if effect can’t fail with an exception. For methods that might return failed IO we should use io.attemptT.There’s more on error handling later.
  • We can also lift F[A] as Left with EitherT.left. It also doesn’t handle errors.
  • EitherT.pure / EitherT.rightT are for wrapping a pure value into EitherT as Right and EitherT.leftT as Left.

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 Optionor 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!

--

--