This is the sixth major release in the Cats Effect 3.x lineage. It is fully binary compatible with every 3.x release, and fully source-compatible with every 3.2.x release.
This release sees several major improvements and fixes to the library, but despite that, most of the pull requests in this release cycle were focused on improving documentation and general library convenience. We've made a lot of progress in this area but there's still plenty of work to do; come join us! Writing a bit of scaladoc or contributing a paragraph of explanation to the website is a great way to dip your toes into contribution, as the many invaluable authors in this release cycle can attest.
Notable Changes
Fixes for guarantee
(and bracket
) When Used with Monad Transformers
In previous versions, guaranteeCase
(and by extension, bracketCase
) was inconsistent with Fiber#join
, despite the fact that both produce an Outcome
representing the results of some effect. This inconsistency was particularly jarring when observed through the lens of monad transformers which have internal short-circuiting, such as OptionT
or EitherT
. The following snippet demonstrates the problem quite succinctly:
import cats.data.OptionT
import cats.effect._
import cats.effect.syntax.all._
OptionT.none[IO, Unit].guarantee(OptionT.liftF(IO.println("guaranteed?"))) >>
OptionT.liftF(IO.println("definitely not"))
On Cats Effect 3.1.1 and earlier, the above snippet would print nothing at all. The fact that "definitely not" fails to print is probably intuitive: after all, calling flatMap
on None
is equivalent to None
, which is to say the flatMap
is "skipped". However, the fact that guarantee
was decidedly not "guaranteed" was a little weird, and in some cases, could actually result in deadlocks.
This was an oversight in implementation. In fact, the design of Outcome
was always intended to take this possibility into account; it is the reason that Outcome.Succeeded
contains an F[A]
rather than just an A
. Fiber#join
leverages this design, but guaranteeCase
was never adjusted to do so.
This oversight has now been corrected, and a new set of laws relating guaranteeCase
and Fiber#join
have been added. These laws codified a set of assumptions that were already generally being made by consumers of the library (the authors included!), and thus they strengthen the guarantees around Cats Effect's semantics rather than making any changes to them. These added laws also have the effect of clarifying the intended semantics of more complex compound effect types (such as monad transformers or polyfunctorial constructors), which should improve compatibility between such types and the vast middleware ecosystem (such as Fs2).
Experimental async
/await
Syntax
Thanks to the tireless work of @Baccata, Cats Effect has added a new incubator library which implements async
/await
syntax for all types which form an Async
(including IO
). This incubator can be found in the typelevel/cats-effect-cps library. Depending on user adoption and feedback, as well as the ongoing work on the Scala 3 compiler, this functionality will likely be added to the Cats Effect core distribution at some point in the future.
async
/await
syntax has seen a surge of popularity across many language ecosystems in recent years, with JavaScript's implementation being the most notable. Roughly speaking, it is an alternative to for
-comprehensions in which imperative statement flow is rewritten into flatMap
(and similar). For example:
async[IO] {
IO.print("Enter your age: ").await
if (IO.readLine.await.toInt % 2 == 0)
IO.println("You're an even number of years old!").await
else
IO.println("Unfortunately, your age is very odd").await
}
The above is equivalent to the following written in a more direct style:
val init = IO.print("Enter your age: ") >> IO.readLine
init flatMap { ageS =>
val age = ageS.toInt
if (age % 2 == 0)
IO.println("You're an even number of years old!")
else
IO.println("Unfortunately, your age is very odd")
}
This syntax is supported on Scala 2 using the -Xasync
compiler flag, and on Scala 3 through the use of @rssh's dotty-cps-async library. Let us know what you think!
Fiber Tracing and Enhanced Exceptions
One of the most popular features ever added to Cats Effect 2 was support for asynchronous fiber tracing, and in particular, the automatic enhancement of exception stack traces that came along with it. Building on the original work of @RaasAhsan, @vasilmkd has now brought this functionality to Cats Effect 3, and in the process further improved and optimized its implementation.
Exception in thread "main" java.lang.Throwable: Boom!
at Example$.$anonfun$program$5(Main.scala:25)
at apply @ Example$.$anonfun$program$3(Main.scala:24)
at ifM @ Example$.$anonfun$program$3(Main.scala:25)
at map @ Example$.$anonfun$program$3(Main.scala:24)
at apply @ Example$.$anonfun$program$1(Main.scala:23)
at flatMap @ Example$.$anonfun$program$1(Main.scala:23)
at apply @ Example$.fib(Main.scala:13)
at flatMap @ Example$.fib(Main.scala:13)
at apply @ Example$.fib(Main.scala:13)
at flatMap @ Example$.fib(Main.scala:13)
In particular, one limitation of the Cats Effect 2 tracing implementation is that it is prone to classloader leaks when using it with frameworks like OSGi. This issue has been eliminated in the reimplementation of this functionality by leveraging java.lang.ClassValue
(a concurrent hashtable specialized to java.lang.Class
keys). Several additional features have been added, including significant optimizations to the full tracing mode.
As in Cats Effect 2, the performance impact of tracing is extremely minimal, but still measurable in synthetic benchmarks. For this reason, tracing defaults to on if not reconfigured by setting -Dcats.effect.tracing.mode=none
when launching the application process. You can read more details on the Tracing documentation page.
User-Facing Pull Requests
- #2148 – More efficient
IO#foreverM
(@armanbilge) - #2134 – Add method forwarders to
Resource
(@alexandrustana) - #2143 – Grow and shrink the error callback hashtable (@vasilmkd)
- #2145 – Add
flatTap
toIO
(@armanbilge) - #2126 – Reimplement
IO#guaranteeCase
andIO.bracketFull
(@vasilmkd) - #2107 – Fix
MonadCancel#guaranteeCase
for transformers and harden with new laws (@djspiewak) - #2114 – Added private functions for resetting the global runtime (@djspiewak)
- #2105 – Add syntax for
Supervisor
(@armanbilge) - #2093 – Add
unsafeToPromise
and friends toDispatcher
(@armanbilge) - #1953 – Translate
SyncIO
toF[_]: Sync
(@vasilmkd) - #1772 – Implement
UUIDGen
capability trait (@Wosin) - #1897 – Add
printStackTrace
toConsole
(@kubukoz) - #1903 – Added some convenience methods to
Outcome
(@davidabrahams) - #1923 – Testing utility method to check that
StackOverflowError
is reproducible (@nikitapecasa) - #1945 –
IO#syncStep
(@vasilmkd) - #2101 – Override
map2Eval
forIO
(@vasilmkd) - #2084 – Small, mostly cosmetics changes to
Resource.both
(@igstan) - #2076 – Remove custom
Order
code already released in Cats (@vasilmkd) - #2064 – Port the
StripedHashtableSpec
to ScalaJS (@vasilmkd) - #2147, #2065, #2051 – Tracing (@RaasAhsan, @vasilmkd, @djspiewak)
- #2048 – Load fields into local variables when using them multiple times (@vasilmkd)
- #2041 – Rewrite and improve
ThreadSafeHashtable
(@vasilmkd) - #2010 – Lazy initialization of array based stacks (@vasilmkd)
- #1958 – Added
ResourceApp
(@RaasAhsan) - #1996 – Get rid of the giant
try
/catch
block inIOFiber
(@vasilmkd) - #1980 – Add explicit return type to
IO.interruptible
andIO#attempt
(@lorandszakacs) - #2140, #2132, #2128, #2120, #2106, #2087, #2083, #2080, #2040, #2071, #2070, #2063, #2030, #2019, #2018, #2015, #1991, #1977, #1988, #1986, #1973 – Miscellaneous documentation improvements (@djspiewak, @vasilmkd, @TimWSpence, @igstan, @Baccata, @arosien, @armanbilge, @balthz, @slice, @bastewart, @kubukoz, @tototoshi, @dangerousben, @paualarco)
Special thanks to each and every one of you!