github typelevel/cats-effect v3.0.0-RC1

latest releases: v3.6.1, v3.6.0, v3.6.0-RC2...
pre-release4 years ago

Cats Effect 3.0.0-RC1 is the first release candidate for 3.0.0. It is expected that one or two more release candidates will be required prior to the final release of 3.0.0, but none are specifically planned at this time. If everything goes perfectly and the ecosystem does not uncover problems, the next release will be 3.0.0 itself.

To that end, we will be attempting to maintain backward compatibility within the 3.0.0-RC lineage leading up to 3.0.0 final. This is not a guarantee and we are not enabling MiMa checks, but the intention is that any further release candidates will be binary compatible with RC1 so as to ease the burden on the ecosystem. No further changes are expected to the API, only additions.

What follows are the changes from M5 to RC1. For a more complete summary of the changes generally included in Cats Effect 3.0.0, please see the M1, M2, M3, M4, and M5 release notes.

Major Changes

Async Instance for Resource

One of the original motivating use-cases behind some of the early thinking around Cats Effect 3 was the following: assume you have two Resources, race them such that the winner is produced and remains open while the loser is cleanly finalized. As it turns out, this semantic is not possible on Cats Effect 2 (without manual bespoke implementation work using allocated), and this fact is honestly quite limiting.

While it is not entirely difficult to implement Resource#race as a special-cased thing, the truly general solution to this limitation, and all similar limitations, is to define Async[Resource[F, *]] given Async[F], which in turn also means defining mechanisms for concurrency in terms of Resource itself. This is a surprisingly tricky problem, since defining the meaning of Fiber in the presence of dynamically-scoped finalizers is not trivial.

The original proposals for Cats Effect 3 involved a proposed typeclass, Region, which was intended to address this issue. In particular, Region defined what it meant to form a dynamic monadic region, in much the same way as Bracket defined what it meant to form a static monadic region. Ultimately, it was decided to remove Region for several reasons, the first of which being that it introduced a considerable amount of complexity into the hierarchy and stood in the way of a number of usability goals.

But even more than usability (which is important!), it was convincingly argued that Region was in fact redundant: every Region can be encoded in terms of an underlying Bracket, and Bracket in turn can be encoded in terms of a Region which is opened and then immediately closed. Thus, Region offered no theoretical expressive power beyond Bracket, which in turn could then be made non-primitive (due to the removal of Region), paving the way for the current uncancelable/onCase/handleErrorWith encoding which exists in the system today.

This argument though implied something very strong: that it was possible to implement the entire typeclass hierarchy for any dynamic monadic region. Now that Async has been implemented for Resource, we have much stronger evidence that this is the case.

The exact semantics of Async[Resource] are subtle in a few ways that are worth calling out:

  • Any scopes that are opened with an async block are maintained after the continuation of the async. This may be a little unintuitive, but it's the safest default given that resources allocated during the registration of an asynchronous action may be critical for the validity of the results once produced.
  • When a resource is started, its finalizers become part of the outer scope. This is to ensure that finalization is run even when the fiber is not joined. There are arguably at least three reasonable semantics which could have been chosen in this area, and in the end, the tie-breaker was that original motivating use case: raceing two Resources and closing the loser.
  • onCancel is very different from Resource.make in that it inserts a finalization boundary which is not extended by flatMap. However, despite this it can still be rendered within Resource through the use of applyFull and allocated, which allow for the encoding of arbitrary scope boundary semantics. (credit to @RaasAhsan for this insight)

As a final note, it is worth calling out the fact that the memoize function, which is available on any Concurrent, does not behave on Resource in the way you might intuitively expect. In particular, if you use the inner Resource which arises from the memoization, the finalizers will be run and the memoized value (which can be re-accessed) may then be invalidated. This is an unavoidable consequence of the lack of linearity in Scala's type system. Ultimately, it is use itself which is unsafe; in its absence, memoize actually behaves as you would expect. use is the operation which cannot be appropriately alias-constrained due to these limitations in Scala, and thus it is to be expected that there are some outsized consequences which cannot be definitively prevented.

Configurable Root Cancelation Scope in MonadCancel

One of the interesting things about cancelation in Cats Effect is that it is a hint. This is somewhat fundamental to the model, which attempts to preserve both safety and preemption, but it is nevertheless interesting and it has some major impacts on the nature of the APIs we can implement. One such impact is the fact that MonadCancel[F].canceled produces an F[Unit], not an F[Nothing], which might be more intuitive. The reason for this is the fact that even self-cancelation is not guaranteed to be respected, since we might be nested within an uncancelable block. Similarly Fiber#cancel is not guaranteed to be immediately respected, only eventually respected if the fiber is not permanently blocked within an uncancelable region.

One perhaps-surprising implication of this is that it is technically possible to validly implement MonadCancel without implementing any cancelation support! This is because it is always safe to ignore the cancelation hint and pretend to be permanently uncancelable. There is still some expressiveness implied by MonadCancel which is more powerful than just MonadError (namely, forceR), but this is relatively narrow in scope.

In this release, we made the decision to allow this "optionality" of MonadCancel to be more directly reflected in the runtime, in the form of the MonadCancel[F].rootCancelScope value. This had a number of helpful consequences, most notable of which the fact that Sync is able to extend MonadCancel without forcing implementations such as SyncIO to implement cancelation without fibers. This in turn is very helpful because it allows users and frameworks to write code which is safe in the presence of cancelation, but which does not assume cancelability.

Splitting these semantics apart generalizes MonadCancel just enough that these kinds of cases are possible, and we avoid ugly pathologies like F[_]: MonadCancel: Sync, which was not uncommon in many corners of the ecosystem prior to this change.

Added the Unique Typeclass

At first glance, Unique appears to be a relatively ad-hoc class to add into the hierarchy. It defines a single method, unique, which produces an F[Unique.Token], where Hash[Unique.Token] is defined. It has only one law, which defines that all sequencings of unique must produce differentiably unique tokens. In other words, arbitrary unique identity.

Again, this seems like a relatively arbitrary thing to add into the hierarchy until you dig into it a little more deeply. For starters, uniquely differentiable values imply some relatively strong things about the effect type, F. It must not memoize, and if the values are differentiable using the JVM's own inherent notion of identity (which is the fastest method for achieving this property in general), it must have some notion of lazy evaluation. These are relatively stringent properties, and they are in fact must stronger than even Defer.

What is also interesting is the fact that Spawn already implies that F must have these properties. In particular due to the fact that Fiber instances are unique by definition: starting multiple times results in independent Fibers, all of which have independent cancel and join methods, and so on. Spawn certainly implies a number of things that are stronger than just the properties necessary to represent uniqueness, but it's at least something in that direction.

This, combined with the fact that uniqueness is a property of the JVM and JavaScript runtimes is relatively strong evidence that this is something that, like Clock, should be reflected in the abstractions. Unique fills that niche, and in so doing, enables the kinds of patterns that were already necessary within libraries like Vault and Fs2, now natively within the Cats Effect runtime calculus.

User-Facing Pull Requests

Special thanks to each and every one of you!

Extra-special thanks are due to @TimWSpence, who has been tirelessly tackling the task of documenting Cats Effect 3!

Don't miss a new cats-effect release

NewReleases is sending notifications on new releases.