More Effect Constructors
Earlier in this chapter, we saw how to use the ZIO.attempt constructor to convert procedural code to ZIO effects.
The ZIO.attempt constructor is a useful and common effect constructor, but it’s not suitable for every scenario:
- Fallible. The
ZIO.attemptconstructor returns an effect that can fail with any kind ofThrowable(ZIO[Any, Throwable, A]). This is the right choice when you are converting legacy code into ZIO and don’t know if it throws exceptions, but sometimes, we know that some code doesn’t throw exceptions (like retrieving the system time). - Synchronous. The
ZIO.attemptconstructor requires that our procedural code be synchronous, returning some value of the specified type from the captured block of code. But in an asynchronous API, we have to register a callback to be invoked when a value of typeAis available. How do we convert asynchronous code to ZIO effects? - Unwrapped. The
ZIO.attemptconstructor assumes the value we are computing is not wrapped in yet another data type, which has its own way of modeling failure. But some of the code that we interact with return anOption[A], anEither[E, A], aTry[A], or even aFuture[A]. How do we convert these types into ZIO effects?
Fortunately, ZIO comes with robust constructors that handle custom failure scenarios, asynchronous code, and other common data types.
Pure Versus Impure Values
Section titled “Pure Versus Impure Values”Before introducing other ZIO effect constructors, we need to first talk about referential transparency. An expression such as 2 + 2 is referentially transparent if we can always replace the computation with its result in any program while still preserving its runtime behavior.
For example, consider this expression:
val sum: Int = 2 + 2We could replace the expression 2 + 2 with its result, 4 and the behavior of our program would not change.
In contrast, consider the following simple program that reads a line of input from the console and then prints it out to the console:
import scala.io.StdIn
val echo: Unit = { val line = StdIn.readLine() println(line)}We can’t replace the body of echo with its result and preserve the behavior of the program. The result value of echo is just the Unit value, so if we replace echo with its return value, we would have:
val echo: Unit = ()These two programs are definitely not the same. The first one reads input from the user and prints the input to the console, but the second program does nothing at all!
The reason we can’t substitute the body of echo with its computed result is that it performs side effects. It does things on the side (reading from and writing to the console). This is in contrast to referentially transparent functions, which are free of side effects and which just compute values.
Expressions without side effects are called pure expressions, while functions whose bodies are pure expressions are called pure functions.
The ZIO.attempt constructor takes side-effecting code, and converts it into a pure value, which merely describes side-effects.
To see this in action, let’s revisit the ZIO implementation for our echo program:
import zio._
val readLine = ZIO.attempt(StdIn.readLine())
def printLine(line: String) = ZIO.attempt(println(line))
val echo = for { line <- readLine _ <- printLine(line) } yield ()This program is referentially transparent because it just builds up a blueprint (an immutable value that describes a workflow) without performing any side effects. We can replace the code that builds up this blueprint with the resulting blueprint, and we still just have a plan for this echo program.
So we can view referential transparency as another way of looking at the idea of functional effects as blueprints. Functional effects make side-effecting code referentially transparent by describing their side-effects, instead of performing them.
This separation between description and execution untangles the what from the how, and gives us enormous power to transform and compose effects, as we will see over the course of this book.
Referential transparency is an important concept when converting code to ZIO because if a value or a function is referentially transparent, then we don’t need to convert it into a ZIO effect. However, if it’s impure, then we need to convert it into a ZIO effect by using the right effect constructor.
ZIO tries to do the right thing even if you accidentally treat side-effecting code as pure code. However, mixing side-effecting code with ZIO code can be a source of bugs, so it is best to be careful about using the right effect constructor. As a side benefit, this will make your code easier to read and review for your colleagues.
Effect Constructors for Pure Computations
Section titled “Effect Constructors for Pure Computations”ZIO comes with a variety of effect constructors to convert pure values into ZIO effects. These constructors are useful primarily when combining other ZIO effects, which have been constructed from side-effecting code, with pure code.
In addition, even pure code can benefit from some features of ZIO, such as environment, typed errors, and stack safety.
The two most basic ways to convert pure values into ZIO effects are succeed and fail:
object ZIO { def fail[E](e: => E): ZIO[Any, E, Nothing] = ??? def succeed[A](a: => A): ZIO[Any, Nothing, A] = ???}The ZIO.succeed constructor converts a value into an effect that succeeds with that value. For example, ZIO.succeed(42) constructs an effect that succeeds with the value 42. The failure type of the effect returned by ZIO.succeed is Nothing because the effects created with this constructor cannot fail.
The ZIO.fail constructor converts a value into an effect that fails with that value. For example, ZIO.fail(new Exception) constructs an effect that fails with the specified exception. The success type of the effect returned by ZIO.fail is Nothing because the effects created with this constructor cannot succeed.
We will see that effects that cannot succeed, either because they fail or because they run forever, often use Nothing as the success type.
In addition to these two basic constructors, there are a variety of other constructors that can convert standard Scala data types into ZIO effects.
import scala.util.Try
object ZIO { def fromEither[E, A](eea: => Either[E, A]): IO[E, A] = ??? def fromOption[A](oa: => Option[A]): IO[None.type, A] = ??? def fromTry[A](a: => Try[A]): Task[A] = ???}These constructors translate the success and failure cases of the original data type to the ZIO success and error types.
The ZIO.fromEither constructor converts an Either[E, A] into an IO[E, A] effect. If the Either is a Left, then the resulting ZIO effect will fail with an E, but if it is a Right, then the resulting ZIO effect will succeed with an A.
The ZIO.fromTry constructor is similar, except the error type is fixed to Throwable because a Try can only fail with Throwable.
The ZIO.fromOption constructor is more interesting and illustrates an idea that will come up often. Notice that the error type is None.type. This is because an Option only has one failure mode. Either an Option[A] is a Some[A] with a value, or it is a None with no other information.
So an Option can fail, but there is essentially only one way it could ever fail—with the value None. The type of this lone failure value is None.type.
These are not the only effect constructors for pure values. In the exercises at the end of this chapter, you will explore a few of the other constructors.
Effect Constructors for Side Effecting Computations
Section titled “Effect Constructors for Side Effecting Computations”The most important effect constructors are those for side-effecting computations. These constructors convert procedural code into ZIO effects, so they become blueprints that separate the what from the how.
Earlier in this chapter, we introduced ZIO.attempt. This constructor captures side-effecting code and defers its evaluation until later, translating any exceptions thrown in the code into ZIO.fail values.
Sometimes, however, we want to convert side-effecting code into a ZIO effect, but we know the side-effecting code does not throw any exceptions. For example, checking the system time or generating a random variable are definitely side effects, but they cannot throw exceptions.
For these cases, we can use the constructor ZIO.succeed, which converts procedural code into a ZIO effect that cannot fail:
object ZIO { def succeed[A](a: => A): ZIO[Any, Nothing, A]}Converting Async Callbacks
Section titled “Converting Async Callbacks”A lot of code in the JVM ecosystem is non-blocking. Non-blocking code doesn’t synchronously compute and return a value. Instead, when you call an asynchronous function, you must provide a callback, and then later, when the value is available, your callback will be invoked with the value. (Sometimes, this is hidden behind Future or some other asynchronous data type.)
For example, let’s say we have the non-blocking query API shown in the following snippet:
def getUserByIdAsync(id: Int)(cb: Option[String] => Unit): Unit = ???If we give this function an id that we are interested in, it will look up the user in the background, but return right away. Then later, when the user has been retrieved, it will invoke the callback function that we pass to the method.
The use of Option in the type signature indicates that there may not be a user with the id we requested.
In the following code snippet, we call getUserByIdAsync and pass a callback that will simply print out the name of the user when it is received:
getUserByIdAsync(0) { case Some(name) => println(name) case None => println("User not found!")}Notice that the call to getUserByIdAsync will return almost immediately, even though it will be some time (maybe even seconds or minutes) before our callback is invoked, and the name of the user is actually printed to the console.
Callback-based APIs can improve performance because we can write more efficient code that doesn’t waste threads. But working directly with callback-based asynchronous code can be quite painful, leading to highly nested code, making it difficult to propagate success and error information to the right place, and making it impossible to handle resources safely.
Fortunately, like Scala’s Future before it, ZIO allows us to take asynchronous code and convert it to ZIO functional effects.
The constructor we need to perform this conversion is ZIO.async, and its type signature is shown in the following snippet:
object ZIO { def async[R, E, A]( cb: (ZIO[R, E, A] => Unit) => Any ): ZIO[R, E, A] = ???}The type signature of ZIO.async can be tricky to understand, so let’s look at an example.
To convert the getUserByIdAsync procedural code into ZIO, we can use the ZIO.async constructor as follows:
def getUserById(id: Int): ZIO[Any, None.type, String] = ZIO.async { callback => getUserByIdAsync(id) { case Some(name) => callback(ZIO.succeed(name)) case None => callback(ZIO.fail(None)) } }The callback provided by async expects a ZIO effect, so if the user exists in the database, we convert the username into a ZIO effect using ZIO.succeed and then invoke the callback with this successful effect. On the other hand, if the user does not exist, we convert None into a ZIO effect using ZIO.fail, and we invoke the callback with this failed effect.
We had to work a little to convert this asynchronous code into a ZIO function, but now we never need to deal with callbacks when working with this query API. We can now treat getUserById like any other ZIO function and compose its return value with methods like flatMap, all without ever blocking and with all of the guarantees that ZIO provides us around resource safety.
As soon as the result of the getUserById computation is available, we will just continue with the other computations in the blueprint we have created.
Note here that in async, the callback function may only be invoked once, so it’s not appropriate for converting all asynchronous APIs. If the callback may be invoked more than once, you can use the async constructor on ZStream, discussed later in this book.
The final constructor we will cover in this chapter is ZIO.fromFuture, which converts a function that creates a Future into a ZIO effect.
The type signature of this constructor is as follows:
def fromFuture[A](make: ExecutionContext => Future[A]): Task[A] = ???Because a Future is a running computation, we have to be quite careful in how we do this. The fromFuture constructor doesn’t take a Future. Rather, the constructor takes a function ExecutionContext => Future[A], which describes how to make a Future given an ExecutionContext.
Although you don’t need to use the provided ExecutionContext when you convert a Future into a ZIO effect, if you do use the context, then ZIO can manage where the Future runs at higher levels.
If possible, we want to make sure that our implementation of the make function creates a new Future instead of returning a Future that is already running. The following code shows an example of doing just this:
def goShoppingFuture( implicit ec: ExecutionContext): Future[Unit] = Future(println("Going to the grocery store"))
val goShoppingZIO: Task[Unit] = ZIO.fromFuture(implicit ec => goShoppingFuture)There are many other constructors to create ZIO effects from other data types, such as java.util.concurrent.Future, and third-party packages to provide conversion from Monix, Cats Effect, and other data types.
