An issue I raised recently with Cats Effect, around WriterT logging not working well with the concurrent parEvalMap in FS2, revealed that the thinking on Cats MTL is shifting.
Cats MTL consists of 2 things:
- Capability traits like
Raise, Tell, Handle and Stateful. These are attached to an abstract effect type F[_] to give it an additional capability. For the 4 traits listed, these capabilities are raising errors, emitting logs, handling errors, and read/writing state, respectively.
- Implementations of these capability traits, in terms of monad transformer (MT) "stacks" that include an MT suited to the capability;
WriterT for emitting logs with Tell, for example.
The documentation currently emphasizes these implementations as the primary role of the library. The homepage leads with the somewhat underwhelming:
Provides transformer typeclasses for cats' Monads, Applicatives and Functors.
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.
However, recent thinking sees more value in the capability traits than the implementations, for at least 2 reasons:
-
It turns out that MTs prove troublesome in larger, industrial-scale programs that use CE3's Concurrent & friends. WriterT[IO.. losing logs in parEvalMap hints at problem with Concurrent instance for WriterT ? cats-effect#3765 is one example of a rash of issues reported in the last couple of years where MTs do surprising things in the presence of concurrency, and consensus is that these surprises are not "fixable". Therefore, we see advice being issued against MT usage, at least in more demanding scenarios.
-
However, many capability traits can be usefully implemented even without MTs, in ways that are better suited to industrial software development, by backing the trait with a mutable cats.effect.Ref for Tell & Stateful, or by "submarine error propagation" in the case of Raise and Handle.
Pleasingly, abstraction of the capability is maintained, in that the trait can be implemented either by Ref, or by MT, without touching application code written in terms of the capability. This allows eg unit tests to use a simpler MT-based solution, while larger applications use a Ref approach.
An example implementation of a Ref-backed Tell:
def refTell[F[_]: Sync, L, M[_]](ref: Ref[F, M[L]])(using a: Applicative[M], mo: Monoid[M[L]]): Tell[F, L] =
new Tell[F, L]:
def tell(l: L): F[Unit] = ref.update(mo.combine(_, a.pure(l)))
def functor = Functor[F]
The shifting thinking around MTs affects the role that Cats MTL plays in the Typelevel ecosystem. The capability traits are the gold. We ought to:
- Revise the Homepage and Getting Started to emphasize the value of abstract capabilities, and deemphasize MTs as the only way to fulfill such capability. Instead mention MT, Refs and "submarine error propagation" as implementation options.
- Show example code on non-MT solutions.
Such a rebranding, perhaps accompanied by a blog post at https://typelevel.org/blog/, could see Cats MTL become relevant to a wider range of Typelevel users and lead to a well-deserved increase in library adoption.
An issue I raised recently with Cats Effect, around WriterT logging not working well with the concurrent
parEvalMapin FS2, revealed that the thinking on Cats MTL is shifting.Cats MTL consists of 2 things:
Raise,Tell,HandleandStateful. These are attached to an abstract effect typeF[_]to give it an additional capability. For the 4 traits listed, these capabilities are raising errors, emitting logs, handling errors, and read/writing state, respectively.WriterTfor emitting logs withTell, for example.The documentation currently emphasizes these implementations as the primary role of the library. The homepage leads with the somewhat underwhelming:
Provides transformer typeclasses for cats' Monads, Applicatives and Functors.
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.
However, recent thinking sees more value in the capability traits than the implementations, for at least 2 reasons:
It turns out that MTs prove troublesome in larger, industrial-scale programs that use CE3's
Concurrent& friends. WriterT[IO.. losing logs in parEvalMap hints at problem with Concurrent instance for WriterT ? cats-effect#3765 is one example of a rash of issues reported in the last couple of years where MTs do surprising things in the presence of concurrency, and consensus is that these surprises are not "fixable". Therefore, we see advice being issued against MT usage, at least in more demanding scenarios.However, many capability traits can be usefully implemented even without MTs, in ways that are better suited to industrial software development, by backing the trait with a mutable
cats.effect.RefforTell&Stateful, or by "submarine error propagation" in the case ofRaiseandHandle.Pleasingly, abstraction of the capability is maintained, in that the trait can be implemented either by Ref, or by MT, without touching application code written in terms of the capability. This allows eg unit tests to use a simpler MT-based solution, while larger applications use a Ref approach.
An example implementation of a Ref-backed Tell:
The shifting thinking around MTs affects the role that Cats MTL plays in the Typelevel ecosystem. The capability traits are the gold. We ought to:
Such a rebranding, perhaps accompanied by a blog post at https://typelevel.org/blog/, could see Cats MTL become relevant to a wider range of Typelevel users and lead to a well-deserved increase in library adoption.