Functional Effects as Blueprints
The core data type in the ZIO library is ZIO[R, E, A], and values of this type are called functional effects.
A functional effect is a kind of blueprint for a concurrent workflow, as illustrated in Figure →. The blueprint is purely descriptive in nature and must be executed in order to observe any side-effects, such as interaction with a database, logging, streaming data across the network, or accepting a request.
A functional effect of type ZIO[R, E, A] requires you to supply a value of type R if you want to execute the effect (this is called the environment of the effect), and when it is executed, it may either fail with a value of type E (the error type), or succeed with a value of type A (the success type).
We will talk more about each of these type parameters shortly. But first, we need to understand what it means for an effect to be a blueprint.
In traditional procedural programming, we are used to each line of our code directly interacting with the outside world. For example, consider the following snippet:
val goShoppingUnsafe: Unit = { println("Going to the grocery store")}As soon as Scala computes the unit value for the goShoppingUnsafe variable, the application will immediately print the text “Going to the grocery store” to the console.
This is an example of direct execution, because in constructing a value, our program directly interacts with the outside world.
This style of programming is called procedural programming, and is familiar to almost all programmers since most programming languages are procedural in nature.
Procedural programming is convenient for simple programs. But when we write our programs in this style, what we want to do (going to the store) becomes tangled with how we want to do it (going to the store now).
This tangling can lead to lots of boilerplate code that is difficult to understand and test, painful to change, and fraught with subtle bugs that we won’t discover until production.
For example, suppose we don’t actually want to go to the grocery store now but in an hour from now. We might try to implement this new feature by using a ScheduledExecutorService:
import java.util.concurrent.{ Executors, ScheduledExecutorService }import java.util.concurrent.TimeUnit._
val scheduler: ScheduledExecutorService = Executors.newScheduledThreadPool(1)
scheduler.schedule( new Runnable { def run: Unit = goShoppingUnsafe }, 1, HOURS)scheduler.shutdown()In this program, we create an executor, schedule goShoppingUnsafe to be executed in one hour, and then shut down the scheduler when we are done. (Don’t worry if you don’t understand everything that is going on here. We will see that ZIO has much easier ways of doing the same thing!)
Not only does this solution involve boilerplate code that is difficult to understand and test and painful to change, but it also has a subtle bug!
Because goShoppingUnsafe is directly executed, rather than being a blueprint for a workflow, “Going to the grocery store” will be printed to the console as soon as goShoppingUnsafe is loaded by the JVM. So we will be going to the grocery store now instead of an hour from now!
In fact, the only thing we have scheduled to be executed in an hour is returning the Unit value of goShoppingUnsafe, which doesn’t do anything at all.
In this case, we can solve the problem by defining goShoppingUnsafe as a def instead of a val to defer its evaluation until later. However, this approach is fragile and error-prone and forces us to think carefully about when each statement in our program will be evaluated, which is no longer the order of the statements.
We also have to be careful not to accidentally evaluate a statement too early. We might assign it to a value or put it into a data structure, which could cause premature evaluation.
It is as if we want to talk to our significant other about going shopping, but as soon as we mention the word “groceries”, they are already at the door!
The solution to this problem (and most problems in concurrent programming) is to make the statements in our program values that describe what we want to do. This way, we can separate what we want to do from how we want to do it.
The following snippet shows what this looks like with ZIO:
import zio._
val goShopping = ZIO.attempt(println("Going to the grocery store"))Here, we are using the attempt constructor to build the goShopping functional effect. The effect is a blueprint that describes going to the store but doesn’t actually do anything right now. (To prove this to yourself, try evaluating the code in the Scala REPL!)
In order to go to the store, we have to execute the effect, which is clearly and forcibly separated from defining the effect, allowing us to untangle these concerns and simplifying code tremendously.
With goShopping defined this way, we can now describe how independent from what, which allows us to solve complex problems compositionally by using operations defined on ZIO effects.
Using the delay operator that is defined on all ZIO effects, we can take goShopping and transform it into a new effect, which will go shopping an hour from now:
val goShoppingLater = goShopping.delay(1.hour)Notice how easy it was for us to reuse the original effect, which specified what, to produce a new effect, which also specified when. We built a solution to a more complex problem by transforming a solution to a simpler problem.
Thanks to the power of describing workflows as ordinary immutable values, we never had to worry about how goShopping was defined or about evaluating it too early. Also, the value returned by the delay operator is just another description, so we can easily use it to build even more sophisticated programs in the same way.
In ZIO, every ZIO effect is just a description—a blueprint for a concurrent workflow. As we write our program, we create larger and more complex blueprints that come closer to solving our business problem. When we are done and have an effect that describes everything we need to do, we hand it off to the ZIO runtime, which executes the blueprint and produces the result of the program.
So, how do we actually run a ZIO effect? The easiest way is to extend the ZIOAppDefault trait and implement the run method, as shown in the following snippet:
import zio._
object GroceryStore extends ZIOAppDefault { val run = goShopping}As you are experimenting with ZIO, extending ZIOAppDefault and implementing your own program logic in the run method is a great way to see the output of different programs.
