github typelevel/cats-effect v3.0.0-M1

latest releases: v3.7.0-RC1, v3.6.3, v3.6.2...
pre-release5 years ago

Cats Effect 3.0.0-M1 is the very first milestone release in the 3.0.0 line. We are expecting to follow this milestone with subsequent ones as necessary to further refine functionality and features, prior to some number of release candidates leading to a final release. The designation "M1" is meant to indicate our relative confidence in the functionality and API of the library, but it by no means indicates completeness or final compatibility. Neither binary compatibility nor source compatibility with anything are assured prior to 3.0.0, though major breakage is unlikely at this point.

Acknowledgements

Normally, these go at the end. In this case, they deserve to be up front and center. So many people jumped in to contribute to this project, particularly over the past several months. It's impossible to properly everyone, and inevitably some names get left off of any list, but it's important to try.

Cats Effect 3 has been a group effort from beginning to end. In particular, the following individuals have been completely indispensable:

  • Raas Ahsan
  • Jakub Kozłowski
  • Fabio Labella
  • Michael Pilquist
  • Ben Plommer
  • Tim Spence
  • Vasil Vasilev

Take a bow, all of you. You deserve it.

Overview

Cats Effect 3 is a complete redesign of the Cats Effect library, starting from first principles and building up from there. It includes:

  • A reorganized, safer, and easier-to-understand typeclass hierarchy
  • A brand new IO implementation that is safer, more convenient, and dramatically faster
  • Redesigned cancellation and evaluation semantics to improve composability
  • Completely novel fiber scheduling implementations on both the JVM and JavaScript
  • ...and much more!

Typeclasses

The hierarchy has been revamped from top to bottom. Several major design principles behind this redesign:

  • Safety
  • Composability
  • Orthogonality
  • Convenience

Those who are familiar with the Cats Effect 2 hierarchy will notice several differences immediately. Bracket is gone, and in its place is MonadCancel, which defines behavior in terms of several lower-level and more powerful operations. bracket itself is still present, but more general and more composable across different datatypes.

Spawn and Concurrent are now nearly at the top of the hierarchy, whereas in Cats Effect 2, Concurrent sits under Async. This properly reflects the fact that concurrency is a generalization of control flow. Just as Monad characterizes sequential control flow, Spawn characterizes control flow in which sequential fibers can fork and later rejoin, and Concurrent characterizes control flow patterns in which fibers can pass data between each other and even create cycles.

Temporal and Clock replace the old Cats Effect 2 Clock and Timer classes, which sat awkwardly outside of the main hierarchy. It is no longer necessary to write functions like the following:

def useful[F[_]: Concurrent: Timer: ContextShift](...)

Instead, you would just write:

def useful[F[_]: Temporal](...)

Speaking of which, ContextShift is gone, never to return. Its existence was necessitated quite indirectly by the fact that the async function scheduled its continuation on the thread which invoked its callback. This seemingly-minor implementation detail has profound implications for the entire library, and in turn gives rise to what is far-and-away the most common "Cats Effect gotcha": forgetting to shift after async. In Cats Effect 3, it is no longer possible to "forget" to shift back to the appropriate thread pool, and it is no longer necessary to manually propagate confusing implicit instances like ContextShift[F] or even ContextShift[IO].

Finally, Sync and Async sit at the very bottom of the hierarchy, reflecting the fact that they are the most powerful typeclasses in existence: they allow users to capture arbitrary side-effects. You can encode any concept you want with these abstractions, which is why we no longer pretend that there are things which are somehow more powerful (such as Concurrent in Cats Effect 2).

In practice, this should result in a hierarchy which is much cleaner and safer for users. It will be easier to tell, at a glance, what a function can or cannot do, since things that are Concurrent are not actually capable of capturing side-effects.

The operations provided by these classes are dramatically simpler, defined in terms of a small set of easy-to-understand primitives. It's always much harder to understand a game with many rules, and Cats Effect 3 is designed to be a game with very few, very powerful rules. To that end, all of the typeclass laws have been completely rewritten, reflecting their simplicity and power. As an example, the law which defines how cancellation behaves is simply the following:

  def canceledAssociatesLeftOverFlatMap[A](fa: F[A]) =
    F.canceled >> fa.void <-> F.canceled

The above can be read as: if a fiber is cancelled, then all subsequent actions are short-circuited and discarded.

One part of this redesign involves a brand new operation, uncancelable, which makes it possible to composably define complex resource management scenarios in which certain parts of a critical region must be interruptible (such as atomically acquiring and releasing a semaphore, where the acquisition may block the fiber). The design of this operation (and the various tradeoffs involved) is explored in depth by Fabio Labella on this excellent issue.

In reflection of their fundamental nature, the typeclass abstractions have been entirely split from IO and now live in a separate, more fundamental module: kernel. Third-party datatype implementations (such as Monix Task) no longer need to rely on Cats Effect IO as part of their fundamental calculus, and end-users who choose to use such datatypes can now reliably have a single datatype on their classpath, rather than two.

IO

The IO monad in Cats Effect 3 has been completely rewritten from the ground up. While it superficially behaves the same as the old one in many circumstances, the implementation is much safer and more compact. Significant emphasis has been placed on user experience, discoverability, and pleasantries. A simple program:

import cats.effect._
import cats.syntax.all._

import scala.concurrent.duration._

object Hello extends IOApp.Simple {

  def greeter(word: String): IO[Unit] =
    (IO.println(word) >> IO.sleep(100.millis)).foreverM

  val run =
    for {
      english <- greeter("Hello").start
      french <- greeter("Bonjour").start
      spanish <- greeter("Hola").start

      _ <- IO.sleep(5.seconds)
      _ <- english.cancel >> french.cancel >> spanish.cancel
    } yield ()
}

This program runs identically on the JVM with Scala 2.12, 2.13, and the latest Dotty milestone, 0.27.0-RC1. It similarly runs on JavaScript with the same ScalaJS versions.

This program superficially resembles the Cats Effect 2 equivalent, but under the surface things are dramatically different. A brand new, fiber-aware, work-stealing thread pool is included as part of Cats Effect 3. This scheduler results in dramatic performance benefits relative to Cats Effect 2 when the application is under heavy, concurrent load: around 1.5x - 5x faster in conservative benchmarks. Unlike microbenchmark improvements to flatMap and map, these are real gains which will show up in service-level performance metrics in production. This scheduler also makes it possible to implement auto-yielding semantics (a form of weakly-preemptive multitasking) with literally zero performance penalty, even ignoring amortization.

Even better-still, this scheduler is fiber-aware, meaning that it understands the common patterns found in Cats Effect programs and optimizes accordingly. This can be a difficult performance benefit to measure reliably, so it's probably better if you just try it in your application and see the benefits for yourself. In general, the results should include things like considerably better cache locality and dramatically lower context shift overhead, particularly as load increases. The scheduler performs best when your application is fully saturated at capacity.

Of course, flatMap and map are also quite a bit faster than in Cats Effect 2, clocking in around 2x - 3x faster, depending on the test. This has very little impact on most real-world applications, but it's still nice to know.

JavaScript also received a huge amount of attention in this rewrite. During the course of its development, Cats Effect IO's test suite revealed an unexpected limitation in the ExecutionContext instance used by nearly all ScalaJS applications: it doesn't correctly yield control to the macrotask event queue. This surprising fact means that simple applications like the following are broken in Cats Effect 2, and their equivalents are broken in nearly every other ScalaJS application:

// cats effect 3 api
IO.cede.foreverM.start.flatMap(f => IO.sleep(5.seconds) >> f.cancel)

This program spawns a fiber which loops forever, always yielding control back to the event queue on each iteration. The main fiber then waits five seconds before canceling this loop and shutting down.

In Cats Effect 2, and in most ScalaJS applications, this program runs forever because the ExecutionContext simply never yields and the timer is never able to fire. Cats Effect 3 fixes this problem with a brand new JavaScript fiber scheduler. This scheduler runs with extremely high performance on NodeJS and all modern browsers, with polyfills to ensure fallback compatibility with all platforms on which ScalaJS itself is supported.

Standard Library (and more!)

Cats Effect 2 was originally designed to be a brutally minimalist library, to the point where even simple utilities like fromFuture were considered to be out of scope. This reflected a general philosophy of extreme composability: build the simplest, most atomic unit and allow the ecosystem to put the pieces together to assemble a masterpiece. This ethos remains in Cats Effect 3, but greater emphasis has been placed on having "batteries included" in the fundamental experience.

To this end, we've begun fleshing out a cats-effect-std module, which currently includes Console (ported from the excellent console4cats library) and will include several other common application requirements, including a fast asynchronous Queue and similar elements.

Additionally, several long-requested utilities, such as fromCompletableFuture, are now available out of the box on both IO and generic F types characterized by the typeclasses. We expect to expand this set of convenience functionality in the run-up to the final 3.0.0 release.

Migration

As much as possible, Cats Effect 3 was designed to minimize breakage. Binary compatibility was not a goal in any way, but moderate source compatibility was maintained whenever possible. After all, the Cats Effect ecosystem is quite large, and no one would want to migrate if it were painful! Some things simply had to change in order to improve, but in most cases, migration is as simple as changing your dependency information and deleting all instances of ContextShift and Timer.

The largest conceptual change that will impact abstract library code is the rearrangement of the typeclass hierarchy. Code which used to require Concurrent but used it to capture side-effects will now need to require Async. In most cases, though, this will result in a tightening of constraints, where code which required Concurrent but didn't need its unsafe functionality is no longer forced to accept the corresponding consequences.

Additionally, for mixed-mode codebases which are unable to use IOApp (e.g. when Cats Effect usage is embedded within a pre-existing unsafe system), there are some major changes involved in running an IO instance. Fortunately, we feel all of those changes are for the better. Here, for example, is a snippet which can be copy/pasted into an ammonite repl and results in a running IO:

import $ivy.`org.typelevel::cats-effect:3.0.0-M1`
import cats.effect._

implicit val runtime = unsafe.IORuntime.global

val program: IO[Unit] = IO.println("Hello, World")
program.unsafeRunSync()

That's it! There is no longer any need to define ContextShift, create thread pools, or otherwise mess around with confusing implementation details.

Onward

We want to hear from you! If you're a library author, now is very much the time to begin playing around with the Cats Effect 3 release. We want to hear about any problems you have, any features you need, and any experiences you want to report! There is still plenty left to do in Cats Effect itself, and many things that we want to implement and polish and enhance. We can't do this without the ecosystem!

The current working plan for Cats Effect as a project is to release 3.0 final immediately following the final stable release of Scala 3, sometime very early 2021. Simultaneously, we will release a fully Scala 3-compatible build of Cats Effect 2 (retaining full source compatibility with Cats Effect 2 on Scala 2). Support for Cats Effect 2 will be maintained for a long time into the future, though the exact sunset date is still TBD. We understand that binary- and source-breaking migrations are painful, and we want to make this transition as easy as possible. We would encourage other libraries within the Cats Effect ecosystem to follow this same pattern (a series/2 and series/3 release branch, maintained in parallel), though we do acknowledge that this is a significant maintenance burden and many projects may wish to simply move entirely to Cats Effect 3.

If you're an end user, now is a good time to start playing around with Cats Effect 3 and seeing whether or not you like it. We want to hear from you, too! If there are things you've always wanted in Cats Effect, now is the time to present your case! However, most Cats Effect applications depend on a large set of dependencies above and beyond Cats Effect itself (unsurprisingly! it is a very rich ecosystem). While some of these dependencies (such as fs2) are already providing Cats Effect 3-compatible releases, not all have been able to make that jump to date.

We hope this situation will change significantly by the time Cats Effect 3.0.0-RC1 is released. For end-user early adopters, RC1 should be the first major Cats Effect 3 release which can be reasonably depended upon by a non-trivial application.

Don't miss a new cats-effect release

NewReleases is sending notifications on new releases.