A tour of ZIO
28 Aug 2022 by dzlab
There are lot of libraries that makes it easy to develop concurrent applications on the JVM, most notably Akka that uses the Actor model.
In fact, Akka actors can be used to solve a lot of challenges, but they also have high implications:
- Requires modeling the application in terms of actors and their interactions in terms of message passing
- Leads to complex code as everything in the application is an Actor
- Requires creating a hierarchy of classes representing the commands that every actor can handle
- Needs coupling between source/destination by passing around an
ActorRef
to send messages - Testing is not straightforward as you need to send message and block till actor respond then assert, sometimes timeout happens which leads to unstable tests
- In general implies partial functions, mutability, special messaging syntax, supervision strategies, lifecycle management, actor systems, defining messaging protocols.
Alternatively to Akka, other libraries provides concurrency primitives that can be used to achieve similar functionality. For instance ZIO/Cats Effect, which in addition to make developing concurrent applications easy and because they are purely functional they also provide improved type safety, immutability, and purity.
Here are some interesting talks about moving away from Akka Actor to more functional alternatives:
- Replacing Actors with Cats Effect and FS2 - article link
- Moving from Akka to ZIO - video link
Choosing between ZIO and Cats Effect depends on your taste of functional programing. For more details on the comparison between the two libraries you can check the following Redit thread about evolving to ZIO or Cats Effects - link.
In Short:
- Cats Effect seems to be purely functional as it is based on ideas from Haskell
- ZIO is simpler and is object-oriented in addition to be functional
- ZIO effects are scala Future but ++ (they are an execution plan)
- ZLayer makes it easy to follow OOP modularity principles
In the rest of this article we will focus on ZIO and the features it provides:
Modularity with ZIO
One of the big advantages of ZIO compared to Cats Effect is the support of Modularity which is an important Object Oriented Paradigm.
ZIO allows the creation of modular code thanks what’s called ZLayer
which can be composed, have dependencies which can be injected by specific implementations.
For example to create a ZLayer
out of a simple service, we first create an interface of the API exposed by the service and provide an implementation as follows:
// define service
trait ServiceA {
def process(input: String): IO[ErrorType, OutputType]
}
// implement service
final case class ServiceAImpl() extends ServiceA {
def process(input: String): IO[ErrorType, OutputType] = … // business logic here
}
Then we create a ZLayer
of the interface that uses the implementation like this
object ServiceAImpl {
val layer: ULayer[Has[ServiceA]] = (ServiceAImpl.apply _).toLayer
}
Notice how we are lifting the Service implementation into a ZLayer
using the toLayer
method.
Here is a more complex example of a service ServiceC
that depends on other services ServiceA
and ServiceB
// define service
trait ServiceC {
def process(input: String): IO[ErrorType, OutputType]
}
// implement service C that depends on Service A and B
final case class ServiceCImpl(a: ServiceA, b: ServiceB) extends ServiceC {
def process(input: String): IO[ErrorType, OutputType] = … // business logic here
}
The we lift the service implementation to a ZLayer
as follows:
object ServiceCImpl {
val layer: URLayer[Has[ServiceA] with Has[ServiceB], Has[ServiceC]] = (ServiceCImpl(_, _)).toLayer
}
We can simplify the use of the service by creating some helpers that create ZIO services
// How to use the services to create a ZIO effect
object ServiceC {
def processWithA(input: String): ZIO[Has[ServiceA], ErrorType, OutputType] = ZIO.serviceWith[ServiceA](_.parse(input))
def processWithC(input: String): ZIO[Has[ServiceC], ErrorType, OutputType] = ZIO.serviceWith[ServiceC](_.parse(input))
}
Note: this code snippet uses ZIO version 1.x, in ZIO version 2.x this is simplified.
Synchronous / Asynchronous with ZIO effects
ZIO effect are all about Asynchronous (non-blocking) logic which is the basis of concurrency. But ZIO effects can also wrap synchronous (blocking) code so that it runs it on a dedicated thread pool. Here are some examples of making ZIO effect out of blocking or non-blocking code:
Synchronous code can be converted into a ZIO effect using ZIO.attempt:
val readLine: ZIO[Any, Throwable, String] = ZIO.attempt(StdIn.readLine())
ZIO has a blocking thread pool built into the runtime, and To execute effects there with ZIO.blocking or:
val sleeping = ZIO.attemptBlocking(Thread.sleep(Long.MaxValue))
Asynchronous code that exposes a callback-based API can be converted into a ZIO effect using ZIO.async:
object legacy {
def login(onSuccess: User => Unit, onFailure: AuthError => Unit): Unit = ???
}
val login: ZIO[Any, AuthError, User] = ZIO.async[Any, AuthError, User] { callback =>
legacy.login( user => callback(ZIO.succeed(user)), err => callback(ZIO.fail(err)) )
}
For more examples check the documentation - link
Concurrency with ZIO fibers
With ZIO, creating asynchronous and concurrent code becomes an easy busiess. At its core, the concurrency in ZIO is based on the Join-Fork pattern. Furthermore, for efficiency ZIO does not uses Threads but instead uses Fibers which are lighter and more efficient than Threads.
Here is an example of concurrency with fork and join which returns the fiber success/fail
for {
fiber <- ZIO.succeed("Hi!").fork // forking an effect creates a fiber from current one
message <- fiber.join // join this fiber with main one
} yield message
Here is another exmaple of concurrency with fork and await which returns Exit value (information on how the fiber completed)
for {
fiber <- ZIO.succeed("Hi!").fork // forking an effect creates a fiber from current one
exit <- fiber.await // join this fiber with main one
} yield exit
For more examples check the documentation - link.
To learn more about fibers and project loom which introduced them check this article - link.
Resources with ZIO
Interacting with external services (e.g. Databases) is handled in ZIO with what is called Resources
which were handled differently between version 1 and version of 2 of ZIO.
Old way with ZManaged
In ZIO version 1, resources were wrapped with in a ZManaged
type. For instance, the following example shows how to manage File resources ZManaged
:
def file(name: String): ZManaged[Any, Throwable, File] = ???
file(name).use { file =>
???
}
Similarly to any other ZIO concept, we can compose ZManaged resources as follows:
for {
file1 <- file(path1)
file2 <- file(path2)
} yield (file1, file2)
New way with Scope
In verion 2 of ZIO, the type ZManaged
was removed and managing resources becomes much easier thanks to ZIO scopes.
Here is an example of how to manage resources using dynamic Scopes:
def file(path: String): ZIO[Scope, Throwable, File] = ???
ZIO.scoped {
file(path).flatMap(useFile)
}
Because Resources are simply ZIO effect, we can now compose them like we compose any other ZIO effect as follows:
for {
file1 <- file(path1)
file2 <- file(path2)
} yield (file1, file2)
To learn more about how Scopes replaced ZManged
check this video and this article
ZIO SQL
ZIO SQL is a relatively new library that provides a ZIO way for connecting and interacting with databasses
- Type safe: catch errors in the query at compile, e.g. syntax errors
- SQL-like DSL: feels like writing sql
- ZIO integration: you get a ZIO effect
- Connection, session, resource and transactional management
- More
Here are some examples of using ZIO SQL to perform different SQL operations
// inserting into a table
insertInto(persons)(id ++ name ++ age).values(List((1, "Charles", 30), (2, "Martin", 28), (3, "Harvey", 42)))
// joining two tables
select(firstName ++ orderDate).from(customers.join(orders).on(id === customerId))
// selecting with subquery
val subquery = customers.subselect(Count(orderId)).from(orders).where(customerId === id)
val query = select(fName ++ lName ++ (subquery as "Count")).from(customers)
Note: For now it seems that ZIO SQL supports only PostgresSQL as a database.
You can learn more about ZIO SQL in this video - link. Another intersting library with ZIO support is Quill, you can check about how it integrates with ZIO here - link.
References
Here is a non-exhaustive list of resources to learn more about ZIO and other frameworks for building concurrent applications on the JVM:
- ZIO
- Cats Effect
- FS2