ZIO & Cats Effect: a successful alliance

Cats Effect has become a kind of “Reactive Streams” for the functional Scala-world, allowing you to combine the entire diverse ecosystem of libraries together.



Many excellent libraries: http4s, fs2, doobie - are implemented only on the basis of type classes from Cats Effect. And libraries like ZIO and Monix, in turn, provide instances of these type classes for their effect types. Despite some problems that will be fixed in version 3.0, Cats Effect helps many open source contributors to organically support the entire functional ecosystem of the Scala language. Developers who use Cats Effect are faced with a difficult choice: which implementation of effects to use for their applications.



Today there are three alternatives:





In this post I will try to prove to you that to create your application using Cats Effect, ZIO is a good choice with design solutions and capabilities that are quite different from the reference implementation in Cats IO.



1. Better MTL / Tagless-Final Architecture



MTL (Monad Transformers Library) is a programming style in which functions are polymorphic by their type of effect and express their requirements through a “type class constraint”. In Scala, this is often called the tagless-final style (although it’s not the same thing), especially when the type class has no laws.



It is well known that it is not possible to define a global instance for such classic MTL type classes as Writer and State, as well as for effect types such as Cats IO. The problem is that instances of these type classes for these types of effects require access to a mutable state, which cannot be created globally, because creating a mutable state is also an effect.



For best performance, however, it is important to avoid "monad transformers" and provide the Write and State implementation directly, on top of the main effect type.



To achieve this, Scala programmers use a trick: create with effects (but cleanly) instances at the top level of their programs and then provide them further in the program as local implications:



Ref.make[AppState](initialAppState).flatMap(ref => implicit val monadState = new MonadState[Task, AppState] { def get: Task[AppState] = ref.get def set(s: AppState): Task[Unit] = ref.set(s).unit } myProgram )
      
      





Despite the fact that such a trick is useful, it is still a “crutch”. In an ideal world, all instances of type classes could be coherent (one instance per type), and not be created locally, generating effects, so that they would then magically wrap themselves in implicit values ​​for use by subsequent methods.



A great feature of MTL / tagless-final is that you can directly define most instances on top of the ZIO data type using the ZIO environment.



Here is one way to create a global MonadState definition for a ZIO data type:



 trait State[S] { def state: Ref[S] } implicit def ZIOMonadState[S, R <: State[S], E]: MonadState[ZIO[R, E, ?], S] = new MonadState[ZIO[R, E, ?], S] { def get: ZIO[R, E, S] = ZIO.accessM(_.state.get) def set(s: S): ZIO[R, E, Unit] = ZIO.accessM(_.state.set(s).unit) }
      
      





An instance is now defined globally for any environment that supports at least State[S]



.



Similarly for FunctorListen



, otherwise known as MonadWriter



:



 trait Writer[W] { def writer: Ref[W] } implicit def ZIOFunctorListen[W: Semigroup, R <: Writer[W], E]: FunctorListen[ZIO[R, E, ?], W] = new FunctorListen[ZIO[R, E, ?], W] { def listen[A](fa: ZIO[R, E, A]): ZIO[R, E, (A, W)] = ZIO.accessM(_.state.get.flatMap(w => fa.map(a => a -> w))) def tell(w: W): ZIO[R, E, W] = ZIO.accessM(_.state.update(_ |+| w).unit) }
      
      





And of course, we can do the same for MonadError



:



 implicit def ZIOMonadError[R, E]: MonadError[ZIO[R, E, ?], E] = new MonadError[ZIO[R, E, ?], E]{ def handleErrorWith[A](fa: ZIO[R, E, A])(f: E => ZIO[R, E, A]): ZIO[R, E, A] = fa catchAll f def raiseError[A](e: E): ZIO[R, E, A] = ZIO.fail(e) }
      
      





This technique is easily applicable to other type classes, including tagless-final type classes, instances of which may require generating effects (changes, configurations), testing functions that generate effects (combining environment effects with tagless-final), or anything else easily accessible from the environment .



No more slow monadic transformations! Let’s say “no” to creating effects when initializing instances of the type class, to local implications. No more crutches needed. Direct immersion in pure functional programming.



2. Saving resources for mere mortals



One of the first features of ZIO was interraption — the ability of the ZIO runtime to instantly interrupt any executable effect and guaranteed to free up all resources. A crude implementation of this feature hit Cats IO.



Haskell called such functionality async exception, which allows you to create and efficiently use latency, efficient parallel and competitive operations, and globally optimal calculations. Such interruptions not only bring great benefits, but also pose complex tasks in the field of supporting secure access to resources.



Programmers are used to tracking errors in programs through simple analysis. This can also be done with ZIO, which uses a type system to help detect errors. But interruption is something else. An effect created from many other effects can be interrupted at any border.



Consider the following effect:



 for { handle <- openFile(file) data <- readFile(handle) _ <- closeFile(handle) } yield data
      
      





Most developers will not be surprised at this scenario: closeFile



will not be executed if readFile



crashes. Fortunately, the effects system has a ensuring



( guarantee



in Cats Effect) that allows you to add a final handler to the finalizer effect, similar to finally.



So, the main problem of the code above can be easily solved:



 for { handle <- openFile(file) data <- readFile(handle).ensuring(closeFile(handle)) } yield ()
      
      





Now the effect has become “fall resistant,” in the sense that if the readFile



breaks, the file will still be closed. And if readFile



succeeds, the file will also be closed. In all cases, the file will be closed.



But still not quite in all. Interruption means that the effect can be interrupted everywhere, even between openFile



and readFile



. If this happens, the open file will not be closed and a resource leak will occur.



The pattern of getting and releasing a resource is so widespread that ZIO introduced a bracket operator, which also appeared in Cats Effect 1.0. The Bracket statement protects against interruptions: if the resource is received successfully, then release will occur even if the effect using the resource is interrupted. Further, neither the receipt nor the release of the resource can be interrupted, thus providing a guarantee of resource security.



Using bracket, the example above would look like this:



 openFile(file).bracket(closeFile(_))(readFile(_))
      
      





Unfortunately, bracket encapsulates only one (rather general) resource consumption pattern. There are many others, especially with competitive data structures, access to which must be accessible for interrupts, otherwise leaks are possible.



In general, all interrupt work comes down to two main things:





ZIO has the ability to implement both. For example, we can develop our version of bracket using low-level ZIO abstractions:



 ZIO.uninterruptible { for { a <- acquire exit <- ZIO.interruptible(use(a)) .run.flatMap(exit => release(a, exit) .const(exit)) b <- ZIO.done(exit) } yield b }
      
      





In this code, use(a)



is the only part that can be interrupted. The surrounding code guarantees release



execution in any case.



At any time, you can check if there is an opportunity for interruptions. For this, only two primitive operations are needed (all the rest are derived from them).



This compositional full-featured interrupt model allows you to implement not only a simple implementation of bracket, but also the implementation of other scenarios in resource management, in which a balance is found between the advantages and disadvantages of interrupts.



Cats IO provides only one operation for controlling interrupts: the uncancelable combinator. It makes the whole block of code uninterrupted. Although this operation is rarely used, it can lead to a resource leak or locks.



At the same time, it turns out that you can define a primitive inside Cats IO, which allows you to achieve more control over interrupts. Fabio Labella's very complicated implementation turned out to be extremely slow.



ZIO allows you to write code with interruptions, operating at a high level with declarative compound operators, and does not force you to choose between severe complexity combined with low performance and blocking leaks.



Moreover, the recently added Software Transactional Memory in ZIO allows the user to declaratively write data structures and code that are automatically asynchronous, competitive and allow interruptions.



3. Guaranteed Finalizers



The try / finally block in many programming languages ​​provides the guarantees that are needed to write synchronous code without leaking resources.



In particular, this block guarantees the following: if a try block starts execution, then the finally block will execute when try stops.



This warranty applies to:





The ZIO “ensuring” operation can be used just like try / finally:



 val effect2 = effect.ensuring(cleanup)
      
      





ZIO provides the following guarantees for "effect.ensuring (finalizer)": if "effect" began to be executed, then "finalizer" will start execution when "effect" stops.



Like try / finally, these guarantees remain in the following cases:





Moreover, the guarantee is maintained even if the effect is interrupted (guarantees on the “bracket” are similar, in fact, “bracket” is implemented on “ensuring”).



The Cats IO data type provides another, weaker guarantee. For “effect.guarantee (finalizer)”, it is weakened as follows: if “effect” began to be executed, “finalizer” will start execution when “effect” stops, if the problem effect is not inserted into the “effect”.



A weaker guarantee is also found in the implementation of the “bracket” in Cats IO.



To get a resource leak, just use the effect used inside the "guarantee" or "bracket.use" effect, compose it with something like this:



 //   `interruptedFiber` -    val bigTrouble = interruptedFiber.join
      
      





When bigTrouble is inserted in this way into another effect, the effect becomes uninterrupted - no “finalizers” set via “guarantee” or cleaning of resources through “bracket” will be executed. All this leads to a drain of resources, even when there is a “finalizer” in the block.



For example, the “finalizer” in the following code will never start execution:



 (IO.unit >> bigTrouble).guarantee(IO(println("Won't be executed!!!«)))
      
      





When evaluating the code without taking into account the global context, it is impossible to determine whether an effect, such as “bigTrouble”, will be inserted anywhere in the “use” effect of the “bracket” operation or inside the finalizer block.



Therefore, you won’t be able to find out if the Cats IO program will work with resource leaks or missing “finalizer” blocks without evaluating the entire program. The whole program can only be evaluated manually, and this process is always accompanied by errors that cannot be verified by the compiler. In addition, this process must be repeated every time any important changes in the code occur.



ZIO has a custom implementation of “guarantee” from Cats Effect, “guaranteeCase” and “bracket”. Implementations use native ZIO semantics (not Cats IO semantics), which allows us to evaluate possible problems with resource leaks here and now, knowing that in all situations “finalizers” will be launched and resources will be freed.



4. Stable switching



Cats Effect has the “evalOn” method from “ContextShift”, which allows you to move the execution of some code to another execution context.



This is useful for a number of reasons:





The “EvalOn” operation performs the effect where it should be run, and then returns to the original execution context. For example:



 cs.evalOn(kafkaContext)(kafkaEffect)
      
      





Note: Cats IO has a similar “shift” construct, which allows you to switch to a different context without going back, but in practice, this behavior is rarely required, so “evalOn” is preferred.



The ZIO implementation of “evalOn” (made on the ZIO primitive “lock”) provides the guarantees necessary to uniquely understand where the effect works — the effect will always be executed in a specific context.



Cats IO has a different, weaker guarantee - the effect will be executed in a certain context until the first asynchronous operation or internal switching.



Considering a small piece of code, it is impossible to know for sure whether an asynchronous effect (or nested switching) will be built into the effect that will switch, because asynchrony is not displayed in types.



Therefore, as in the case of resource security, to understand where the Cats IO effect will be launched, it is necessary to study the entire program. In practice, and from my experience, Cats IO users are surprised when, when using “evalOn” in one context, it is subsequently discovered that most of the effect was accidentally performed in another.



ZIO allows you to determine where effects should be triggered, and trust that this is how it will happen in all cases, no matter how the effects are built into other effects.



5. Security of error messages



Any effect that supports concurrency, concurrency, or secure access to resources will run into a problem with the linear error model: in general, not all errors can be saved.



This is true for both `Throwable`, a fixed error type built into Cats IO, and the polymorphic error type supported by ZIO.



Examples of situations with multiple one-time errors:





Since not all errors are saved, ZIO provides a “Cause [E]” data structure based on a free semiring (an abstraction from abstract algebra, its knowledge is not supposed here), which allows connecting serial and parallel errors for any type of error. During all operations (including cleaning for a fallen or interrupted effect), ZIO aggregates errors into the “Cause [E]” data structure. This data structure is available at any time. As a result, ZIO always stores all errors: they are always available, they can be logged, studied, transformed, as required by business requirements.



Cats IO chose a model with loss of error information. While ZIO will connect the two errors through Cause [E], Cats IO will “lose” one of the error messages, for example, by calling “e.printStackTrace ()” on the error that occurs.



For example, an error in the “finalizer” in this code will be lost.



 IO.raiseError(new Error("Error 1")).guarantee(IO.raiseError(new Error("Error 2«)))
      
      





This approach to error tracking means that you cannot locally locate and process the entire spectrum of errors that occur due to the combination of effects. ZIO allows you to use any type of error, including “Throwable” (or more specific subtypes like “IOExceptio” or another custom exception hierarchy), ensuring that no errors are lost during program execution.



6. Asynchrony without deadlocks



Both ZIO and Cats IO provide a constructor that allows you to take code with a callback and wrap it in effect



This feature is provided through the Async pipe class in Cats Effect:



 val effect: Task[Data] = Async[Task].async(k => getDataWithCallbacks( onSuccess = v => k(Right(v)), onFailure = e => k(Left(e)) ))
      
      





This creates an asynchronous effect, which, when executed, will wait until the value appears, and then continue, and all this will be obvious to the user of the effect. Therefore, functional programming is so attractive for developing asynchronous code.



Notice that as soon as the callback code turns into an effect, the callback function (here it is called `k`) is called. This callback function exits with a success / error value. When this callback function is called, the execution of the effect (previously paused) resumes.



ZIO guarantees that the effect will resume execution on the runtime thread pool if the effect was not assigned to any particular special context, or to another context to which the effect was attached.



Cats IO resumes the effect on the callback thread. The difference between these options is quite deep: the thread causing the callback does not expect the callback code to be executed forever, but only allows a slight delay before the control returns. On the other hand, Cats IO does not give such a guarantee at all: the calling thread, the starting callback, may freeze, waiting for an indefinite time when the control of execution returns.



Early versions of the competitive data structures in Cats Effect (“Deferred”, “Semaphore”) resumed effects that did not return control to the calling thread. As a result, problems were discovered in them related to deadlocks and a broken execution scheduler. Although all these problems have been found, they are only fixed for competitive data structures in Cats Effect.



User code that uses a similar approach as in Cats IO will get into such troubles, because such tasks are non-deterministic, errors can only occur very rarely, in runtime, making debugging and problem detection a difficult process.



ZIO provides deadlock protection and a normal task scheduler out of the box, and also makes the user explicitly choose the behavior of Cats IO (for example, using "unsafeRun" on "Promise", which ended in a resumed asynchronous effect).



Although none of the solutions is suitable for absolutely all cases, and ZIO and Cats IO provide enough flexibility to solve all situations (in different ways), choosing ZIO means using “Async” without any worries and forces you to put the problem code in “unsafeRun”, which is known to cause deadlock



7. Compatible with Future



Using "Future" from the Scala standard library is a reality for a large number of code bases. ZIO comes with a “fromFuture” method, which provides a ready-made execution context:



 ZIO.fromFuture(implicit ec => // Create some Future using `ec`: ??? )
      
      





When this method is used to wrap Future in an effect, ZIO can set where Future will be executed, and other methods, such as evalOn, will correctly transfer Future to the desired execution context. Cats IO accepts "Future", which was created with an external "ExecutionContext". This means that Cats IO cannot move the execution of Future according to the requirements of the evalOn or shift methods. Moreover, this burdens the user with determining the execution context for Future, which means narrow selection and a separate environment.



Since the provided ExecutionContext can be ignored, ZIO can be represented as the sum of the Cats IO features, guaranteeing a smoother and more accurate interaction with Future in the general case, but there are still exceptions.



8. Blocking IO



As was shown in the article “ Thread Pool. Best practices with ZIO ”, for server applications, at least two separate pools are required for maximum efficiency:





The decision to run all effects on a fixed thread pool will one day lead to deadlock, while triggering all effects on a dynamic pool can lead to performance loss.



On the JVM, ZIO provides two operations that support blocking effects:





, , , «blocking». , - , , «effectBlocking» , ZIO ( ).



Cats IO , . , «blocking», «evalOn», , , .



( ZIO) (, ), .



9.



, Scala, :





, (, http4s «Kleisli» «OptionT»). («effect totation»), ZIO «reader» «typed error» ZIO. «reader» «typed error» , ZIO , . , «Task[A]», «reader» «typed errors».



ZIO () - . , ZIO , .



Cats IO . , , «reader» «typed errors» «state», «writer» , .



ZIO 8 Cats IO . , Scala .



10.



ZIO , , . , Scala, .



ZIO 2000 , «typed errors» , — 375 . Scala , . , , .



:





. , - , .



- . , . ZIO . Cats IO , , ZIO ( , ).



11.



ZIO , , - .





, Scala, , ZIO , , , ZIO, . Cats IO , Cats.



, , , ( , , ).



12.



ZIO — - , .



:





- Cats IO . Cats IO , ( ) .



Conclusion



Cats Effect Scala-, , .



, Cats Effect, , Cats Effect : Cats IO, Monix, Zio.



, . , , , : ZIO Cats Effect .



Scala — . , Scala. ScalaConf , 18 , John A De Goes .



All Articles