Overview
In this blog we will discuss how the concurrent workflows can be simplified and can be executed in a sequential manner using flatmap and other sequential composition.The flatmap method of ZIO effect represents sequential composition where output of first effect act as input for the second. Further in this blog we will discuss some more operators that can define further workflow. You need to have basic understanding of Scala to continue further in this blog, Check out this page to learn more about Scala.
Flat-Map
A simplified type signature of flatmap will look like this,
trait ZIO[R, E, A] { ... def flatMap[B](andThen: A => ZIO[R, E, B]): ZIO[R, E, B] = ... ... }
In simpler terms flatmap says run the first effect and use the output to run the second one in continuation. Consider the following snippet.
import scala.io.StdIn val readLine = ZIO.effect(StdIn.readLine()) def printLine(line: String) = ZIO.effect(println(line)) val echo = readLine.flatMap(line => printLine(line))
In the above code we can see how flatmap is sequentially reading from the console and doing the next steps on its own. So we are doing two things in sequence here. Let’s see a procedural way of doing this same thing.
val line = Console.readLine Console.println(line) val data = doQuery(query) val response = generateResponse(data) writeResponse(response)
We can translate the above code into ZIO, And it will look like this.
ZIO.effect(doQuery(query)).flatMap(data => ZIO.effect(generateResponse(data)).flatMap(response => ZIO.effect(writeResponse(response)) ) )
The above code is okay and easily readable, but once we nest three or more flatmap operations together code gets harder to read and this thing gets time consuming as well. This is where for comprehensions comes into play.
For Comprehensions
Consider this snippet for example
readLine.flatMap(line => printLine(line))
we can rewrite this in for comprehension and it will look something like this:
import zio._ val echo = for { line <- readLine _ <- printLine(line) } yield ()
Pretty simple and easy to understand, Right? The above code snippet also looks like written in more procedural way. For comprehensions under the hood calls flat map method n – 1 times, where n is the number of lines in for comprehension. At last it calls map method for last remaining effect. For example:
for { x <- doA y <- doB(x) z <- doC(x, y) } yield x + y + z
The above snippet will get translated into a Scala code which will look like this.
doA.flatMap(x => doB(x).flatMap(y => doC(x, y).map(z => x + y + z)))
Some Other Sequential operators
Sequential operations are very much common when we work with ZIO effects, ZIO provides a bunch of operators for these common needs. One of them is zipWith that combines two effects sequentially, it merge two effect by specified user-defined function.
Let’s consider a scenario where we need to take two inputs, that is, First name and Last name and we need to combine them and take it as a single string.
val firstName = ZIO.effect(StdIn.readLine("What is your first name?")) val lastName = ZIO.effect(StdIn.readLine("What is your last name")) val fullName = firstName.zipWith(lastName)((first, last) => s"$first $last")
Remember that zipWith unlike flatmap does not allow second effect to be dependent of the first effect’s output. It still describes left-to-right composition. zipWith comes with other variations as well, that includes zip, zipRight, zipLeft. zip here will combine two effects and result will be a tuple of the result of the two effects. zipRight will combine and return the result of second where as zipLeft will return the result of the first effect after combining two effects sequentially.
val helloWorld = ZIO.effect(print("Hello, ")) *> ZIO.effect(print("World!\n"))
Don’t worry, “*>” this is the type alias for zipRight, Similarly “<*” this here will be the alias for zipLeft.
For-Each and Collect-all
The foreach is having a similar functionality as of for loops we see in procedural programming languages. Let’s try to relate it using an example. If we want to print a range of numbers we can do it simply like this.
val printNumbers = ZIO.foreach(1 to 100) { n => printLine(n.toString) }
Similarly collectAll returns a single effect by sequentially combining a bunch of other effects. Consider the code snippet below that simply prints some string by combing a bunch of strings.
val prints = List( printLine("The"), printLine("quick"), printLine("brown"), printLine("fox") ) val printWords = ZIO.collectAll(prints)
Conclusion
Information that is present in this blog can be used to convert any procedural code into ZIO programs. To read more informational blogs like this check out other amazing Knoldus blogs here. To know more about Knoldus click here.