6

A tour of ZIO

 1 year ago
source link: https://dzlab.github.io/2022/08/28/zio-intro/
Go to the source link to view the article. You can view the picture content, updated content and better typesetting reading experience. If the link is broken, please click the button below to view the snapshot at that time.

A tour of ZIO

28 Aug 2022 by dzlab

zio.png

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:

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

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
    • Introduction to Programming with ZIO Functional Effects - link
    • Mastering Modularity in ZIO with Zlayer - link
    • Polling with ZIO - link
    • ZIO vs Cats Effect - link
    • Zymposium - Idiomatic ZIO App Architecture - Video / Code
    • Awesome ZIO - link
  • Cats Effect
    • Cats Effect Intro - link
    • Cats Effect concepts - link
    • Examples - link
    • Book - link
  • FS2

About Joyk


Aggregate valuable and interesting links.
Joyk means Joy of geeK