What is MTL?

MTL is an acronym and stands for Monad Transformer Library. Its main purpose is to make it easier to work with nested monad transformers. It achieves this by encoding the effects of most common monad transformers as type classes.

The problem

Have you ever worked with two or more monad transformers nested inside each other? If you haven't, working with what some call monad transformer stacks can be incredibly painful. This is because, the more monad transformers you add to your stack the more type parameters the type system has to deal with and the worse type inference gets. For example, here's a small example of a method that reads the current state in StateT and raises an error using EitherT:

import cats.data._

def checkState: EitherT[StateT[List, Int, *], Exception, String] = for {
  currentState <- EitherT.liftF(StateT.get[List, Int])
  result <- if (currentState > 10) 
              EitherT.leftT[StateT[List, Int, *], String](new Exception)
            else 
              EitherT.rightT[StateT[List, Int, *], Exception]("All good")
} yield result

There's a bunch of type annotations and extra machinery that really has nothing to do with the actual program and this problem only gets worse as you add more expressions to your for-comprehension or add an additional monad transformer to the stack.

Thankfully, Cats-mtl is here to help.

How Cats-mtl helps

Monad Transformers encode some notion of effect

EitherT encodes the effect of short-circuiting errors. ReaderT encodes the effect of reading a value from the environment. StateT encodes the effect of pure local mutable state.

All of these monad transformers encode their effects as data structures, but there's another way to achieve the same result: Type classes!

For example take ReaderT.ask function, what would it look like if we used a type class here instead? Well, Cats-mtl has an answer and it's called Ask. You can think of it as ReaderT encoded as a type class:

trait Ask[F[_], E] {
  val applicative: Applicative[F]

  def ask: F[E]
}

At its core Ask just encodes the fact that we can ask for a value from the environment, exactly like ReaderT does. Exactly like ReaderT, it also includes another type parameter E, that represents that environment.

If you're wondering why Ask has an Applicative field instead of just extending from Applicative, that is to avoid implicit ambiguities that arise from having multiple subclasses of a given type (here Applicative) in scope implicitly. So in this case we favor composition over inheritance as otherwise, we could not e.g. use Monad together with Ask.

Ask is an example for what is at the core of Cats-mtl. Cats-mtl provides type classes for most common effects which let you choose what kind of effects you need without commiting to a specific monad transformer stack.

Ideally, you'd write all your code using only an abstract type constructor F[_] with different type class constraints and then at the end run that code with a specific data type that is able to fulfill those constraints. So without further ado, let's look at what the program from earlier looks when using Cats-mtl:

First, the original program again:

import cats.data._

def checkState: EitherT[StateT[List, Int, *], Exception, String] = for {
  currentState <- EitherT.liftF(StateT.get[List, Int])
  result <- if (currentState > 10) 
              EitherT.leftT[StateT[List, Int, *], String](new Exception("Too large"))
            else 
              EitherT.rightT[StateT[List, Int, *], Exception]("All good")
} yield result

And now the mtl version:

import cats.MonadError
import cats.syntax.all._
import cats.mtl.Stateful

def checkState[F[_]](implicit S: Stateful[F, Int], E: MonadError[F, Exception]): F[String] = for {
  currentState <- S.get
  result <- if (currentState > 10) E.raiseError(new Exception("Too large"))
            else E.pure("All good")
} yield result

We've reduced the boilerplate immensely! Now our small program actually looks just like control flow and we didn't need to annotate any types.

This is great so far, but checkState now returns an abstract F[String]. We need some F that has an instance of both Stateful and MonadError. We know that EitherT can have a MonadError instance and StateT can have a Stateful instance, but how do we get both? Fortunately, cats-mtl allows us to lift instances of mtl classes through our monad transformer stack. That means, that e.g. EitherT[StateT[List, Int, *], Exception, A] has both the MonadError and Stateful instances we need, neat!

So let's try turning our abstract F[String] into an actual value:

val materializedProgram =
  checkState[EitherT[StateT[List, Int, *], Exception, *]]

This process of turning a program defined by an abstract type constructor with additional type class constraints into an actual concrete data type is sometimes called interpreting or materializing a program. Usually we don't have to do this until the very end where we want to run the full application, so the only place we see actual monad transformers is at the very edge.

In summary Cats-mtl provides two things: MTL type classes representing effects and a way to lift instances of these classes through transformer stacks.

We suggest you start learning the MTL classes first and learn how lifting works later, as you don't need to understand lifting at all to enjoy all the benefits of Cats-mtl.

Available MTL classes

cats-mtl provides the following "MTL classes":

If you're wondering why some of these are based on Monad and others on Functor or Applicative, it is because these are the smallest superclass dependencies practically possible with which you can define their laws. This is in contrast to Haskell's mtl library and earlier versions of Cats as well as Scalaz, this gives us less restrictions over which type classes can be lifted over which transformers.

Because these type classes are commonly combined, the base typeclasses are ambiguous in implicit scope using the typical cats subclass encoding via inheritance. Thus in cats-mtl, the ambiguity is avoided by using the composition approach that we saw with Ask earlier. In a future version of Scala we might be able to avoid this and have the same type class encoding everywhere.