This is the second installment of the series. The previous article is Comparing effect systems in Scala: The Problem and Future.
Comparing effect systems in Scala
Download full guideHerding Cats and their Effects
The Cats library and the related Cats Effect were inspired by scalaz and scalaz-concurrent libraries (and, more broadly, by Haskell and its ecosystem). They represent the most widely adopted effect system in the Scala ecosystem. In this context, effects are values representing descriptions of computations that can be composed using typeclass-based abstractions like Functor, Monad, or Sync, as provided by Cats and Cats Effect. This approach promotes the use of pure functions and referential transparency - the principle that any expression can be replaced by its evaluated result without changing the program's behavior.
There are two ways to write Cats-based programs: one is to use a concrete effect type like cats.effect.IO directly; the other is to use a tagless final encoding, where computations are written abstractly over some F[_] that satisfies one or more typeclass constraints (e.g., Sync[F], MonadThrow[F]). This follows the “code to an interface” philosophy, with typeclasses serving as algebraic interfaces for effectful capabilities. A related good practice is to use the abstraction of the least power: constrain F[_] only by the minimal capabilities your code truly needs. This keeps programs more modular, interpretable, and testable, and avoids overcommitting to unnecessary effect power (like using Async when Sync would suffice). In industrial practice, it seems that many teams have chosen to reserve tagless final approach for library code and to prefer concrete effect types in application code for simplicity.
We are going to use the former approach and use the concrete IO type in our implementation but the companion git repository for this article contains a tagless final implementation too. It will come handy in a moment in the discussion about how capabilities are tracked in different effect systems.
Here’s how we can implement our design using cats and cats-effect:
The implementation of our algorithm is, in fact, very similar to the previous one. The most important syntactical difference is that almost everything here is an expression returning the IO effect type and therefore there are a lot more flatMaps all over the place. The biggest semantic difference is that this implementation is truly non-blocking - cats-effect offers its own blocking queue implementation that suspends the computation on take when empty and restarts it when there’s a new element available. The queue doesn’t however implement open/closed semantics and this forces us to retain the poison pill mechanic but, in comparison to Future we no longer need to send poison pills on error as the IO.parSequenceN_ combinator will cancel other worker loops for us automatically, providing a much nicer structured concurrency experience.
From the ergonomics perspective, cats and cats-effect depend strongly on implicitly resolved typeclasses and therefore require an import to bring in all the instances and combinators into the scope. This model is very powerful but at the same time depends a lot on experience as the combinators like .sequence or .parTupled are not defined on the types themselves so one has to remember about them and also be able to resolve the compile error with an import of a correct instance (or lack of thereof!). Fortunately, when programming against raw IO this problem is much less pervasive than when dealing with tagless final encoding. Beside this point, the cats ecosystem is nice to work with provided one is already fine with monadic abstractions. The APIs are well thought out and refined through multiple iterations of the design and the cats-effect standard library offers almost everything one would need and if anything is missing, there’s a pretty good chance that it can be found in the Typelevel ecosystem of libraries.
One of the most important innovations that monadic effects introduced was asynchronous cancellation. In the example using standard library’s Future we’ve seen that without it, our only options to reign in runaway processes are external coordination and explicit checkpointing. This is not really a problem with any of the effect frameworks as they implement cancellation protocol that allows them to stop execution at any point where the effect monad is evaluated. This allows us to fulfil our last requirement - that parallel workers are interrupted if any of them encounters an error. We’ll soon see that this property is shared by all other monadic solutions.
Fun with trifunctors - ZIO
ZIO came to being as the result of John A. De Goes attempts to reenergize Scalaz development by introducing an IO monad for the 8th release of the library. ZIO, like cats-effect, is a purely functional solution. Its initial design differed from cats-effect mostly in the shape of the core effect monad - IO[E, A] - which contained a typed error channel. Subsequent split from Scalaz led to further divergence from the Haskell-inspired programming model based on typeclasses and pushed ZIO to become focused on Scala-specific solutions that would improve the ergonomics for users, for example the use of variance available in Scala. The next evolutionary step was the inclusion of the reader monad in the main effect that led to the final shape of ZIO[-R, +E, +A] where R represents the environment of the computation, E the typed error channel and A the type of value produced by the computation. Core ZIO library avoids implicits and doesn’t depend on any typeclasses. The promoted programming style focuses on accessible combinators defined on concrete types, readable naming and breaks with the orthodoxy of function names inherited from Haskell to ease the learning curve. Nowadays ZIO offers a lot of baked-in functionality meant to improve the velocity of teams building robust and reliable systems like software transactional memory or dependency injection mechanism.
Let’s see the implementation using ZIO:
It’s immediately noticeable that it is a very similar code to the one written with cats-effect with concrete IO - all functions return an effect and the user has a plethora of different combinators available to compose them. The most striking syntactical difference is that ZIO prefers to keep its combinators as functions on the ZIO object and the instances of the ZIO effect. This actually makes it quite pleasant to use as discoverability is better than in the case of implicitly resolved extension methods. Another nice touch is that the Queue in ZIO offers open/closed semantics and this allows us to ditch the poison pill protocol completely. The queue.shutdown combinator interrupts all the workers still waiting for the next element to arrive. To avoid the interruption bubbling up and crashing ZIO.collectAllParDiscard with an error we just convert the cause of fiber’s exit into normal exit in case of interruption.
An important bit of effect systems lore in Scala is that ZIO’s fused effect design was an attempt to evade the need for monad transformers. Monad transformers are the canonical way to add new functionality to the effect used throughout the application in the cats-effect world. For example, to express an explicit, typed error channel over cats.effect.IO one would use a monad transformer called EitherT with a type of EitherT[IO, E, A] where E is a type of the error and A is the type of the result of the computation. This approach can and very often is stacked to add additional capabilities to the effect - to showcase that reality - in the cats-effect world to express ZIO’s baked-in capabilities we would end up with a ReaderT[IO, R, EitherT[IO, E, A]] where R is the environment type, E is the typed error channel and A is the result of the computation.
The series continues in the final installment, Comparing effect systems in Scala: Kyo, Gears, and Ox.




