edmundnoble / cats-mtl   0.6.0

GitHub

cats transformer type classes.

Scala versions: 2.13 2.12 2.11 2.10
Scala.js versions: 0.6

cats-mtl

Build Status codecov.io

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.

SBT

libraryDependencies += "org.typelevel" %%% "cats-mtl-core" % "0.0.2"

Typeclasses

Migration guide

Here's a map from 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 to F[_]: Monad: ApplicativeLocal[?[_], E]

The root cause for this is addressed in the motivation section.

MTL classes

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.

Lifting classes

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 in Inner ((Inner ~> Inner, Inner ~> Inner)) to <X>-homomorphisms in M (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.

Why not MonadTrans, etc?

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.

Typeclass summaries

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].

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 to Functor or Applicative whenever possible, with the result that there's now a notion of a Functor transformer stack and Applicative transformer stack in addition to that of a Monad transformer stack.
  • the most used operations on MonadWriter and MonadReader are tell and ask, and the other operations severely restrict the space of implementations despite being used much less. To fix this FunctorListen and ApplicativeLocal are subclasses of FunctorTell and ApplicativeAsk, 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:

typelevel/cats#1210

typelevel/cats#1379

typelevel/cats#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.

Community

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.