Transformer typeclasses for cats.
Provides transformer typeclasses for cats' Monads, Applicatives and Functors.
As well, there are some abstractions thrown in that allow you to generically lift MTL typeclasses through transformers.
You can have multiple cats-mtl transformer typeclasses in scope at once without implicit ambiguity, unlike in pre-1.0.0 cats or Scalaz 7.
libraryDependencies += "org.typelevel" %%% "cats-mtl-core" % "0.2.1"
Here's a map from pre-1.x cats typeclasses to cats-mtl typeclasses:
MonadReader --> ApplicativeLocal
MonadWriter --> FunctorListen
MonadState --> MonadState
FunctorFilter --> FunctorEmpty
TraverseFilter --> TraverseEmpty
cats typeclass parameters and context bounds have to be rewritten, to include base classes. For example:
[F[_]: FunctorFilter]
will have to be adjusted to[F[_]: Functor: FunctorEmpty]
,[F[_]: MonadReader[?[_], E]]
will have to be adjusted toF[_]: Monad: ApplicativeLocal[?[_], E]
The root cause for this is addressed in the motivation section.
cats-mtl provides the following "MTL classes":
ApplicativeAsk
ApplicativeLocal
FunctorEmpty
FunctorListen
FunctorRaise
FunctorTell
MonadState
All of these are typeclasses built on top of other "base" typeclasses to provide extra laws.
Because they are commonly combined, the base typeclasses are ambiguous in implicit scope using
the typical cats subclass encoding via subtyping. Thus in cats-mtl, the ambiguity is avoided by using
an implicit scope with a different priority to house each conversion Subclass => Base
.
Compared to cats and Haskell's mtl library, cats-mtl's typeclasses have the smallest superclass dependencies practically possible and in some cases smaller sets of operations so that they can be lifted over more transformers.
Several other typeclasses are provided to lift typeclasses
through transformer data types, like OptionT
.
They form a hierarchy:
MonadLayerControl
\
||
||
||
||
||
MonadLayerFunctor <----------- ApplicativeLayerFunctor <----------------- FunctorLayerFunctor
\ \ \
|| || ||
|| || ||
|| || ||
|| || ||
|| || ||
|| || ||
|| || ||
|| MonadLayer <---------------- || ApplicativeLayer <------------------- || FunctorLayer
|\______________ \______________ \______________
<X>Layer[M, Inner]
is three things:
- an instance of
<X>[Inner]
and an instance of<X>[M]
- a
FunctionK[Inner, M]
which respects the<X>[Inner]
and<X>[M]
instances - a higher-kinded variant of
cats.Invariant
, which maps<X>
-isomorphisms inInner
((Inner ~> Inner, Inner ~> Inner)
) to<X>
-homomorphisms inM
(M ~> M
)
It lets you lift monadic values into other monads which are "larger" than them,
for example monads that are being used inside monad transformers.
For example, ApplicativeLocal
requires MonadLayer
to lift through a transformer, because
all of its operations are invertible.
<X>LayerFunctor
is a <X>Layer
but instead a variant of cats.Functor
, which maps
<X>
-homomorphisms in Inner
(Inner ~> Inner
) to
<X>
-homomorphisms in M
(M ~> M
).
Mapping natural transformations over the inner Inner
inside M
.
MonadLayerControl
allows you to lift operations that consume M
using the layerControl
method,
which allows one the ability to "unravel" an M[A]
to an Inner[State[A]]
temporarily,
as long as the value produced with it is already in the M[_]
context.
For example, FunctorListen
requires this typeclass to lift through a transformer.
MonadTrans
forces the shape T[_[_], _]
on anything which can "contain" another monad,
but newtyping around an instantiated transformer eliminates that shape. I don't see usecases
in the wild which lift transformations M ~> N
inside a transformer to implement any MTL class,
so the extra power was removed.
From the scaladoc:
ApplicativeAsk[F, E]
lets you access an E
value in the F[_]
context.
Intuitively, this means that an E
value is required as an input to get "out" of the F[_]
context.
ApplicativeLocal[F, E]
lets you substitute all of the ask
occurrences in an F[A]
value with
ask.map(f)
, for some f: E => E
which produces a modified E
input.
FunctorEmpty[F]
allows you to map
and filter out elements simultaneously.
FunctorListen[F, L]
is a function F[A] => F[(A, L)]
which exposes some state
that is contained in all F[A]
values, and can be modified using tell
.
FunctorRaise[F, E]
expresses the ability to raise errors of type E
in a functorial F[_]
context.
This means that a value of type F[A]
may contain no A
values but instead an E
error value,
and further map
calls will not have any values to execute the passed function on.
FunctorTell[F, L]
is the ability to "log" values L
inside a context F[_]
, as an effect.
MonadState[F, S]
is the capability to access and modify a state value
from inside the F[_]
context, using set(s: S): F[Unit]
and get: F[S]
.
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 thisFunctorListen
andApplicativeLocal
are subclasses ofFunctorTell
andApplicativeAsk
, 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[_]: FunctorEmpty
isn't enough
for a Functor[F]
to be visible in implicit scope, despite FunctorEmpty
containing a Functor
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 Functor[F]
instance for the type
from cats will always conflict with a derived instance.
Thus F[_]: FunctorFilter
, translated, becomes F[_]: Functor: FunctorEmpty
.
For some historical info on the origins of cats-mtl, see:
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.
People are expected to follow the Typelevel Code of Conduct when discussing cats-mtl on the Github page, Gitter channel, or other venues.
We hope that our community will be respectful, helpful, and kind. If you find yourself embroiled in a situation that becomes heated, or that fails to live up to our expectations, you should disengage and contact one of the project maintainers in private. We hope to avoid letting minor aggressions and misunderstandings escalate into larger problems.