1. Overview
In this tutorial, we’ll explore how we can create ZIO effects in our Scala applications. We’ll go through how we can create effects directly from values and functions and also how we can go from monadic types in the standard Scala library to a ZIO effect.
2. What Is a ZIO Effect
Before we start creating effects in ZIO, let’s briefly understand what an effect is in ZIO: We can define a functional effect as a single unit of computation in a ZIO system. This unit of computation can either be successful and produce a value or fail with an error. Effects are represented by the data type ZIO which has the type parameters of ZIO[R, E, A] where:
- R = The environment type needs to run the effect
- E = The failure type of the effect
- A = The type expected when the effect returns successfully.
3. Creating an Effect From a Value
The unit function of an effect is ZIO.succeed(). This allows us to provide a value and wrap it in a ZIO effect:
ZIO.succeed(42)
This will return an effect with the type ZIO[Any, Nothing, Int]. Indicating that it requires no environment type to run, does not return a fail type, and returns a success type of Int.
4. Creating a Fail Effect
In the same way, we made a successful ZIO effect. We can also create a failure effect directly, using the ZIO.fail() function and passing it a value:
ZIO.fail("Some failure message")
This returns a type of ZIO[Any, String, Nothing] as it returns a failure of String, but there is no success value possible.
5. Creating an Effect From an Option
To create an effect from a ZIO we can use the ZIO.fromOption() function. This function will return a successful effect for a Some and a failure effect for a None:
val someZio = ZIO.fromOption(Some(42))
val noneZio = ZIO.fromOption(None)
The two calls we’ve made to ZIO.fromOption() return a type of ZIO[Any, Option[Nothing], Int]. However, the someZio will result in a success value of 42, and the noneZio will result in a failure of Option[Nothing].
When we use an Option in our standard Scala code, we usually want to return another value when we get None, using getOrElse. Similarly, in ZIO, a failure of Option[Nothing] (also known as None) isn’t beneficial and doesn’t give us many contexts for whatever our failure was. To solve this problem, we can use .elseFail to convert our Option[Nothing] value to something more valuable:
ZIO.fromOption(None).orElseFail("No value present")
This returns a type of ZIO[Any, String, Nothing], meaning in a failure state, this effect will return a String instead of Option[Nothing]
6. Creating an Effect From an Either
To convert an Either into a ZIO effect, we can use the function ZIO.fromEither():
ZIO.fromEither(Right(42))
An Either converts quite naturally into an effect. Where the Left type of the Either translates to the failure type of a ZIO and the Right type of the Either translates to the success type of the ZIO. For example, if we have an Either[String, Int] and pass it to the ZIO.fromEither() function, we would get the return type of ZIO[Any, String, Int].
7. Creating an Effect From a Try
We can also convert from Scala’s Try using ZIO.fromTry():
ZIO.fromTry(Try(42/0))
As with Either, Try already has a success and exception state, so it naturally converts to a ZIO. In the example above, the Try[Int] would translate into ZIO[Any, Throwable, Int].
8. Creating an Effect From a Future
The last type from the standard library we can convert into a ZIO is a Future. To do this, we call the ZIO.toFuture() function, passing in the Future we want to convert as well as implicitly passing an ExecutionContext so ZIO can decide where the Future is run:
ZIO.fromFuture{implicit ec =>
Future.successful(42)
}
Since the failure state of a Future is a Throwable, this snippet will provide an effect with the type ZIO[Any, Throwable, Int].
9. Creating an Effect From a Code Block
In the same way that we can create a ZIO from a value, we can also create one from a code block using the same ZIO.succeed() function:
def doSomething = 42 + 1
ZIO.succeed(doSomething)
This will return a ZIO with the same success type as the returned type of the function you pass it. In this example, we’d end up with a ZIO[Any, Nothing, Int].
10. Effects From Code Which Throws Exceptions
A lot of the time, when writing programs, it is possible for our code to throw Exceptions. If we know a function might throw an Exception, we tell ZIO that it could produce a Throwable by calling ZIO.attempt():
def doDivision = 42/0
ZIO.attempt(doDivision)
This code will return a ZIO[Any, Throwable, Int] if our code does throw an exception, a failure effect will be returned with the type Throwable. If we know the exact Exception our function will throw, we can refine the Throwable type by calling the refineOrDie() function on the ZIO and specifying the type of Throwable we’d expect our code to throw:
def doDivision = 42/0
ZIO.attempt(doDivision).refineOrDie[ArithmeticException]
This will now return a ZIO[Any, ArithmeticException, Int], so we know the exact Exception we expect this ZIO to throw.
11. Handling Code Which Is Thread Blocking
If we know our code will be thread blocking, we can call ZIO.attemptBlocking() to tell ZIO to treat the effect as blocking code:
def blockingCode = Thread.sleep(1000)
ZIO.attemptBlocking(blockingCode)
If we already have a ZIO[Any, Throwable, A], we can also call ZIO.blocking() to mark the effect as being blocking:
def blockingZIO = ZIO.attempt(Thread.sleep(1000))
ZIO.blocking(blockingZIO)
The benefit of doing this is that ZIO knows this code will block the thread. It won’t be run on the primary thread pool but on a thread pool dedicated to blocking computations.
12. Handling Callback-Style Code
The final scenario we’ll cover is how to handle callback-style code. To start, let’s define a function that takes a success and failure callback, does some computation, and calls the correct callback function based on the result of the computation:
def divideNumbers(num1: Int, num2: Int)(
success: Int => Unit,
failure: String => Unit
) = {
if (num2 == 0) {
failure("Dividing by zero")
} else {
success(num1 / num2)
}
}
ZIO.async[Any, String, Int] {callback =>
divideNumbers(1, 2)(
success => callback(ZIO.succeed(success)),
failure => callback(ZIO.fail(failure))
)
}
In this snippet, we defined divideNumbers which divides two numbers and calls the failure callback if the divisor is zero, otherwise will call the success callback with the result. If we want to convert this into an effect in our code, we can use the ZIO.async() function:
def divideNumbers(num1: Int, num2: Int)(
success: Int => Unit,
failure: String => Unit
) = {
if (num2 == 0) {
failure("Dividing by zero")
} else {
success(num1 / num2)
}
}
ZIO.async[Any, String, Int] {callback =>
divideNumbers(1, 2)(
success => callback(ZIO.succeed(success)),
failure => callback(ZIO.fail(failure))
)
}
Here we are calling ZIO.async() and passing in the call to our function, providing the callbacks as the successful and failure effects, with the values of each being the result of the function. This means we can now treat this function as a normal ZIO effect in our code.
13. Conclusion
In this tutorial, we have covered many of the functions provided by the ZIO library to help us convert values, functions, code blocks, and types from the standard Scala library into ZIO effects. This should give us a robust base to start using ZIO in our existing applications and provide a foundation to continue learning more about ZIO.
As always, the sample code used in this tutorial is available over on GitHub.