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:
- using subtyping to express typeclass subclassing results in implicit ambiguities, and doing it another way would result in a massive inconsistency inside cats if only done for MTL classes. for a detailed explanation, see Adelbert Chang's article here.
- most MTL classes do not actually require
Monad
as a constraint for their laws. cats-mtl weakens this constraint toFunctor
orApplicative
whenever possible, with the result that there's now a notion of aFunctor
transformer stack andApplicative
transformer stack in addition to that of aMonad
transformer stack. - the most used operations on
MonadWriter
andMonadReader
aretell
andask
, and the other operations severely restrict the space of implementations despite being used much less. To fix thisListen
andLocal
are subclasses ofTell
andAsk
, which have only the essentials.
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.