Composing and Chaining Effects together in ZIO

Reading Time: 3 minutes

Introduction to ZIO

ZIO is a library for asynchronous and concurrent programming that is based on pure functional programming.

It combined with Scala help us to develop applications that are purely functional, asynchronous, type-safe, resilient, and testable.

At the core of ZIO is ZIO data type which is defined as:

ZIO[-R, +E, +A] 

ZIO is lazy!

The difference between ZIO and procedural programming is quite straightforward. In the latter, the statements are executed as they are encountered by the compiler.

But in ZIO, we describe effects. The effects we want to produce are described but not executed.

They are executed only at a specific point in our application usually called the end of the world for example the main function.

The end of the world is a specific point in our application where the purely functional part of the program ends and the descriptions of the outside world are being run.

ZIO.effect(println("Hello"))

Here, we simply describe an effect that will print a message. But we don’t actually run it. To run it we have to do the following steps:

object PrintHello extends App {

  override def run(args: List[String]): URIO[zio.ZEnv, ExitCode] = {
    ZIO.effect(println("Hello")).exitCode
  }
}

Composing effects in ZIO

So as we have seen above that in ZIO we only describe effects. What if we describe 2 effects like this and execute them:

object TwoEffects extends App {
  override def run(args: List[String]): URIO[zio.ZEnv, ExitCode] = {
    {
      ZIO.effect(StdIn.readLine())
      ZIO.effect(println("Accepted Input"))
    }.exitCode
  }
}

//OUTPUT: Accepted Input

When we run this code then we are not asked for input. So this can be compared to the below code:

val number: Int = {
3
4
}
println(number)

//OUTPUT: 4

In the above code, adding 3 is useless. It does not play any role in value assignment. A similar thing happens when we put 2 effects together.

This is why we need to compose effects so that all the effects are executed in the way we intend them to.

There are different ways in which we can compose and chain out effects together in ZIO. Below I have listed some of the common ones:

flatMap

object FlatMapComposition extends App {
  val takeInput: Task[String] = ZIO.effect(StdIn.readLine) // ZIO[Any, Throwable, String]
  val greet: String => Task[Unit] = name => ZIO.effect(println(s"Greetings $name! I am using flatMap"))

  override def run(args: List[String]): URIO[zio.ZEnv, ExitCode] = {

    takeInput.flatMap(name => greet(name)).exitCode
  }
}

// INPUT: Prakhar
// OUTPUT: Greetings Prakhar! I am using flatMap

Here, we first execute takeInput and then extract its output and use it in greet

for comprehension

object ForComprehensionComposition extends App {
  val takeInput: Task[String] = ZIO.effect(StdIn.readLine) // ZIO[Any, Throwable, String]
  val greet: String => Task[Unit] = name => ZIO.effect(println(s"Greetings $name! I am using for comprehension"))

  override def run(args: List[String]): URIO[zio.ZEnv, ExitCode] = {
    {
      for {
        name <- takeInput
        _ <- greet(name)
      } yield ()
    }.exitCode
  }
}

// INPUT: Prakhar
// OUTPUT: Greetings Prakhar! I am using for comprehension 

for comprehension are nothing more than a sequence of flatMap and map. It is however easier to read and reason about.

zipWith

object ZipWithComposition extends App {
  val number1: Task[Int] = ZIO.effect(StdIn.readInt()) // ZIO[Any, Throwable, Int]
  val number2: Task[Int] = ZIO.effect(StdIn.readInt())


  override def run(args: List[String]): URIO[zio.ZEnv, ExitCode] = {
    val sum: ZIO[Any, Throwable, String] = number1.zipWith(number2)((n1, n2) => s"Sum: ${n1 + n2}")
    sum.flatMap(result => ZIO.effect(println(result))).exitCode
  }
}

// INPUT: 2 & 3
// OUTPUT: Sum: 5

zipWith extracts output from the 2 effects and forms a tuple and performs a function passed to it which in this case is summing the 2 numbers.

zipRight

object ZipRightComposition extends App {
  def getUserInput(message: String): ZIO[Console, IOException, String] =
    putStrLn(message) *> getStrLn

  override def run(args: List[String]): URIO[zio.ZEnv, ExitCode] = {
    (getUserInput("What is your Name? ").flatMap(name => putStrLn(s"Greetings $name. I am using zipRight"))).exitCode
  }
}

Conclusion

So I hope the readers were able to understand how we can compose and chain effects together in ZIO.

Chaining effects together are extremely important so I recommend the readers to play around with these constructs, look at the data types and try out various combinations to improve learning.

References: https://zio.dev/version-1.x/overview/

knoldus

Leave a Reply