What are Effect System and Why Do We care?
If you are new to functional programming in Scala, you probably have encountered libraries like
cats-effect and ZIO. If you are attentive to the news in the Scala ecosystem, you have also heard that there is even a new kid on the block in the effect system neighbourhood, Kyo. What are they and why do we need them? In this post, we are digging into that question.
If you go to the
cats-effect website it reads: “The pure asynchronous runtime for Scala”, the
zio web site states “Type-safe, composable asynchronous and concurrent programming for Scala”.
If you are not familiar with with effect systems, these statements are probably meaningless to you. Doesn’t Scala run on the JVM, which support asynchronous and concurrent programming already? Why do we need these things?
The Problem With Effects
To understand why we need effect systems, we need to understand the problem they solve, and to understand that problem we need to understand what effects are. “The effect of an expression is a concise summary of the observable side-effects that the expression may have when it is evaluated” 1. For example, if we have the expression:
The observable side-effect is that a text is appended to the console, so the effect is “Write to the Console” or “Input/Output” in a more general way. This is a “side-effect” because the
println function’s action is executed “on the side”. We cannot, in our program, go and get whatever was printed to the console. If we want to verify the side effect, we need to go outside of the program, look at the console and verify that it was printed. This makes effectful code hard to test. Also, in Scala, this expression returns
(), which does not tell anything about the nature of what was printed.
Another example of an effect is modifying state. For example, if we have the expressions:
var x = 1
x = 2 // this is an effectful expression
The observable side effect of
x = 2 is the modification of the value of variable
x. The effect for this operation is commonly known as “State”. The different operations whose side effect is to manipulate state (modify it, read it, change it) are part of the effect “State”. Once again, the modification of the variable happens on the side. We cannot know, from the result of the execution of the statement
x = 2 that the variable was actually updated. In the same way that we did it with the console, we need to go and get the value of the variable to make sure that the state was actually modified. In the same way as before, the assignment expression returns
(), which does not tell us anything about the type of state that was modified.
Other commonly known effects are exceptions, nondeterminism, time, continuations, and their combinations.
The opposite of effectful expressions are pure expressions. Pure expressions serve only to produce a value and do not produce effects. So, the only thing that we can check about pure expression is its return value, and we have the type system that can help us with that.
For example, a pure expression that returns an integer is of type
Int. That means that the compiler can check that the result of that expression is only used in a place where an
Int is expected.
So, effects are sets of operations that perform actions on the side. The fact that the actions performed are on the side, puts them outside of the static checking provided by the compiler. Thus, while effects are necessary to write useful software, using them directly (in “vanilla” Scala) does not offer the guarantees that pure code offers, i.e. having the type checker do verifications for us.
Enter effect systems. The goal of effect systems is to bring the type checker when programming with effectul expressions. Now, there is a fundamental difference between the side-effect of
println and the side-effect of
x = 2. The side-effect of
println is completely in the outside world of the program. There is no way for the program to go an check that outside world without an effectful computation. The
x = 2 assignement is inside the program’s world. There is a way for the program to read that variable when it is in scope.
These two kinds of effects can be represented by monads. However, in the Scala jargon, when we talk about effect systems we are talking about the former, the kind of side effects that are completely in the outside world. For example, database access, serving a website, file system access, writing to the console, concurrent and asynchronous operations etc. are the kind of thing that your effect system will take care of. The most difficult to get right are asynchronous and concurrent operations, thus, a little help from the type checker is always welcome. That is why the slogan of both ZIO and cats,effect make a big emphasis on those concepts.
What can Effect Systems Do For You
As we said before, effect systems will help you to write concurrent and asynchronous programs. However, effects are still returning
() most of the time. Of course, some effects (like reading the contents of a file or getting a row from a database) do return a value. Nevertheless, what can the type system do with the part that happens on the side?
Classical effect systems (like
cats-effect, ZIO or Monix) are based around the IO monad. Since Scala already let’s you write effectful code in direct style (i.e. without using monads), what the IO monad let’s you do, is suspending those effects in the IO. This suspension means that the effects are not executed inmediately, but will be executed at a later time. Doing this, allows to separate the definition of the effectful code and the execution of said code. Hence, the first benefit of effect systems is separation of concerns.
The IO monad provides the standard methods of monads (
point), but different implementations provide methods for common operations. For example,
cats-effect IO o monad has a
println method, or a
sleep method. These methods are more than just convenience methods. As the creators of the effect system have separated execution from definition, they can assign a very precise meaning to the
sleep method. The meaning (or semantics) will be provided by the part of the effect system that runs the effectful code. That part is usually called the runtime. So, the second benefit of effect systems is that they provide an effect DSL (domain specific language) with a precise semantics. The correcness of the code you write in that DSL can be checked to some extend by, you guessed who, the type system.
Combining these two benefits, effect systems can provide extremely optimized runtimes, with precise semantics and all that checked to some extend by the type system. Also, the runtimes can sometimes emulate features that are not even there natively. For example, the JVM only starts supporting virtual (lightweight) threads in Java 21.
cats-effect and ZIO users have had that functionality for a long time and can use it even if they have not yet migrated to the latest JVM.
Safety and Ergonomics
However, effect systems can go beyond that. For example,
cats-effect uses a tagless final style and provides type classes for different aspects of the effect system. For example, if you know you are not using asynchronous code, you can just require the
Sync[F] type class. If you know you that you are going to print to the console, you can use the
Console[F] type class. Using the different type classes provided by
cats-effect you can apply the least powerful construct principle to your code. It has already happened to me that I’m writing purely synchronous code and I need to call a function from a library. The compiler then tells me that to use that function, I need to pass the
Async[F] type class. This warns me that I am introducing asynchronous code in my logic. As such, safety (the fact the system does not allow the user to perform invalid operations) is another benefit of effect systems.
cats-effect approach is modular. If you need error handling, pass around context, you need to combine IO monad with the other effects that you need like Either or Kleisli. Hence, to get more advanced functionality, you need to obtain it by combining existing functionality. The classical solution involves using monad transformers.
Another approach to help users with effectful programming is ZIO. ZIO assumes that you will need to do error handling (or short-circuiting) and pass context around (or dependency injection) and provides you with a turbocharged (Z)IO monad. The advantage here is that having everything in the same monad eliminates the need for monad transformers. This, in turn, helps the compiler to provide better type inference than the one that you get when using monad transformers. So, another advantage of effect systems is that they can provide ergonomics for common use cases.
Also, recently, at the Functional Scala conference a new effect system called Kyo was released. Where ZIO creates a super ZIO monad combining IO, dependency injection and short-circuiting, Kyo allows for an arbitrary number of effects. Thanks to algebraic effects, Kyo aims to provide a more general approach to effect handling, given that algebraic effects are composable. Now that you are familiar with what an effect system like Kyo is supposed to do, you can evaluate yourself. I personally, see it as a very promising framework.
The approach of Kyo is based on algebraic effects. Algebraic effects are a relatively novel concept. Basically, researchers managed to find a general semantics for effects that is defined by equations. This fact has allowed programming language researchers to better understand effects and prove that algebraic effects are freely composable. This composability makes them qualitatively better than the existing monadic effects that are provided by
cats-effets and ZIO, as no monad transformers are needed to compose effects.
To summarize, effect systems can provide:
- separation of concerns (separate the declaration of the effect from its execution)
- DSL with precise semantics
The Future of Effect Systems
In his talk about Direct Style Scala at the Scalar Conference 2023, Odersky shows a glimpse of the future of Scala and how all those monads and monads transformers might be one day replaced by direct Scala code thanks to algebraic effects.
As we have seen above, algebraic effecst are qualitatively better than their monadic counterparts, as they are composable. Libraries as Kyo show what a new encoding for effects might look like in the future and the new features that algebraic effects bring to the table.
If you have read until here and liked this content please consider subscribing to my mailing list. I write regularly about Blockchain Technology and Scala.