scalafunctional-programmingscala-catscategory-theorycats-effect

cats effect evaluates only the final for coprehension and ignores rest


I'm a newbie to functional programming and cats effect. Started practicing cats effect 3 with the below code

package com.scalaFPLove.FabioLabellaTalks

import cats.effect.IOApp
import cats.effect.{IO, Concurrent}
import cats.effect.kernel.Deferred
import cats.implicits._
import cats.kernel
import cats.effect.kernel.Ref
import cats.effect.unsafe.IORuntime.global

object RefAndDeferredConcurrency extends IOApp.Simple {

  override def run: IO[Unit] = {
    for {
      _ <- IO.println("Hello1")
      _ <- IO.println("World1")
    } yield ()
    for {
      _ <- IO.println("Hello2")
      _ <- IO.println("World2")
    } yield ()
  }
}

The output I expected is

Hello1 world1 Hello2 world2

but the actual output is

Hello2 world2

Unable to understand the idea behind this, I tried many different things and only the last for comprehension is evaluated ignoring the rest. please help me understand this.


Solution

  • The simplest explanation is this: IO[A] is like () => A (or () => Future[A]). Except it comes with map and flatMap used in for-comprehension.

    When you have:

    override def run: IO[Unit] = {
      for {
        _ <- IO.println("Hello1")
        _ <- IO.println("World1")
      } yield ()
      for {
        _ <- IO.println("Hello2")
        _ <- IO.println("World2")
      } yield ()
    }
    

    it's basically very similar to:

    def IOprintln(str: String): () => Unit = _ => println(str)
    
    def run: () => Unit = {
      () => {
        val _ = IOprintln("Hello1")()
        val _ = IOprintln("World1")()
        ()
      }
      () => {
        val _ = IOprintln("Hello2")()
        val _ = IOprintln("World2")()
        ()
      }
    }
    

    (In your program run is called in IOApp.Simple's main).

    You see what happened here? We created 2 recipes for a program, but never combined them together, and the value of block of code is the last expression.

    And these expressions with recepies - by the very nature of () => ... - are not computing side effects until you run them. It' very useful because it allow you to do things like:

    def program(condition: Boolean) = {
      val a = IO.println("hello world 1")
      val b = IO.println("hello world 2")
      if (condition) a else b
    }
    

    where our program would print only 1 thing, depending on the condition, but not 2 of them at once before even checking which is needed.

    Basically what would be executed (somewhere, at some point) is the last IO expression. If you build it using flatMap then only the last expression in that flatMap would become part of the recipe, and so on. E.g. your original program can be desugared to:

    override def run: IO[Unit] = {
      IO.println("Hello1").flatMap { _ =>
        IO.println("World1").map { _ =>
          ()
        }
      }
      IO.println("Hello2").flatMap { _ =>
        IO.println("World2").map { _ =>
          ()
        }
      }
    }
    

    and when you remember that

    • IO is a value that someone somewhere evaluates (like a function)
    • when evaluating data you can only evaluate what's become a part of that data
    • the value of a block is the value of it's last expression

    it should be clear what will and what will not become part of a final program.