In the last post, we used the IO class (of cats-effects library) to do side effects or simply effects in a pure function.
In this post, we will practice IO class a bit more. We write some functions that do some very useful and popular stuff on one or more IO objects.Exercise 1:
Sequencing two IO objects and returning the second one. Meaning that we want to write a function that takes two IO objects of type A and B and returns an IO of type B, but only after making sure the IO of A is completed. The method signature should look like this:
def sequenceTakeLast[A, B](ioa: IO[A], iob: IO[B]): IO[B] = ???For writing this function we should remember that the IO class is a monad and it has all the methods that a monad has. Including the flatMap method. There are 2 sub-tasks:
- We want to return the IO of B
- We want to make sure the IO of A is completed first
def sequenceTakeLast[A, B](ioa: IO[A], iob: IO[B]): IO[B] = {
ioa.flatMap(_ => iob.map(b => b))
}As you might know from your Scala learning experience, we can write a second version of this function using for-comprehension:
def sequenceTakeLast[A, B](ioa: IO[A], iob: IO[B]): IO[B] =
for {
_ <- ioa
b <- iob
} yield bThere is another version for this function, using the specific operator of IO class called andThen which is written as *>
def sequenceTakeLast[A, B](ioa: IO[A], iob: IO[B]): IO[B] = ioa *> iob
Exercise 2:
The next exercise is a function to take two different IO objects, exactly like the previous exercise, but instead of returning the second one, it returns the first one.
So we want to return the first one after making sure both IO objects have been completed in sequence.
The function signature looks like this:
def sequenceTakeFirst[A, B](ioa: IO[A], iob: IO[B]): IO[A] =Sub-tasks are:
- We want to return the IO of B
- We want to make sure the IO of A is completed first
def sequenceTakeFirst[A, B](ioa: IO[A], iob: IO[B]): IO[A] =
ioa.flatMap(a => iob.map(_ => a)) def sequenceTakeFirst[A, B](ioa: IO[A], iob: IO[B]): IO[A] =
for {
a <- ioa
_ <- iob
} yield adef sequenceTakeFirst3[A, B](ioa: IO[A], iob: IO[B]): IO[A] = ioa <* iobExercise 3:
def forever[A](ioa: IO[A]): IO[A] = ???def forever[A](ioa: IO[A]): IO[A] = ioa.flatMap(_ => forever(ioa))def forever[A](ioa: IO[A]): IO[A] = ioa >> forever(ioa)You can test these two functions using the following main method:
def main(args: Array[String]): Unit = {
import cats.effect.unsafe.implicits.global
val io = IO(println("hello"))
forever(io).unsafeRunSync()
}You will notice that none of these 2 versions of the forever function causes stackoverflow and it is because the second version is used the by-name version of andThen operator and the first version does not cause stackoverflow just because the flatMap method is using the FlatMap case class which is evaluating the parameter lazily.
If we write the second version using the by-value version of the andThen operator then the program will crash with the stackoverflow error:
Bad version of the function, do not do this:
def forever_badVersion[A](ioa: IO[A]): IO[A] = ioa *> forever_badVersion(ioa)Instead of any of these versions of the forever function, you can simply call foreverM method of the IO class:
def forever[A](ioa: IO[A]): IO[A] = ioa.foreverM
Comments
Post a Comment