cats-mtl Design

Overall, cats-mtl has similar design to cats: instances package, syntax package, implicits package.

There's also a hierarchy package, because cats-mtl uses a different typeclass encoding than cats to avoid implicit ambiguity.

The problem looks like this:

import cats._

type Err
type Log

def f[F[_]: MonadError[?, Err]: MonadWriter[?, Log]]: Unit = {
      // implicitly[Monad[F]] // ambiguity between derived monads from MonadError and MonadWriter
      // 1.pure[F].flatMap(_ => 1.raise) // same ambiguity affects MonadSyntax
      ()
}

The cause is that the implicit conversions MonadError[F, Err] => Monad[F] and MonadWriter[F, Log] => Monad[F] are the same priority. If those conversions were different priorities, the issue would be fixed, and that's exactly what cats-mtl's encoding does.

Similarly to the Scato project, instead of inheritance the transformer classes in cats-mtl use aggregation. Mtl classes thus contain instances of their superclasses.

Motivation

The motivation for cats-mtl's existence can be summed up in a few points:

The first point there means that it's impossible for cats-mtl type classes to expose their base class instances implicitly; for example F[_]: Stateful[?[_], S] isn't enough for a Monad[F] to be visible in implicit scope, despite Stateful containing a Monad instance as a member. The root cause here is that prioritizing implicit conversions with subtyping explicitly can't work with cats and cats-mtl separate, as the Monad[F] instance for the type from cats will always conflict with a derived instance.

Thus F[_]: Stateful[?[_], S], translated, becomes F[_]: Monad: Stateful[?[_], S].

For some historical info on the origins of cats-mtl, see:

https://github.com/typelevel/cats/issues/1210

https://github.com/typelevel/cats/pull/1379

https://github.com/typelevel/cats/pull/1751

Laws

Type class laws come in a few varieties in cats-mtl: internal, external, and free.

Internal laws dictate how multiple operations inter-relate. One side of the equation can always be reduced to a single function application of an operation. These express "default" implementations that should be indistinguishable in result from the actual implementation.

External laws are laws that still need to be tested but don't fall into the internal laws.

Free laws are (in theory) unnecessary to test, because they are implied by other laws and the types of the operations in question. There will usually be rudimentary proofs or some justification attached to make sure these aren't just made up.