Search code examples
scalafunctional-programmingscala-catscats-effect

What does Cats Effect's IO.suspend function really do?


What does cats-effect's IO.suspend do and why it is useful? There's documentation, but it isn't completely clear.

The documentation gives the following use case:

import cats.effect.IO

def fib(n: Int, a: Long, b: Long): IO[Long] =
  IO.suspend {
    if (n > 0)
      fib(n - 1, b, a + b)
    else
      IO.pure(a)
  }

As an example, why would I want to use the above, vs. the following similar function?

import cats.effect.IO

import scala.annotation.tailrec

@tailrec
def fib(n: Int, a: Long, b: Long): IO[Long] =
  if (n > 0)
    fib(n -1, b, a + b)
  else
    IO.pure(a)


Solution

  • One of them is lazy, the other is eager.

    def printFibIfNeeded(n: Int, shouldPrint: Boolean) = {
      val fibResult = fib(n, 0, 1)
      if (shouldPrint) fibResult.map(r => println(r))
      else IO.unit
    }
    

    If you use suspend, then fibResult will be just a recipe for computation that won't be run if shouldPrint. = false.

    In your second example you already computer the value and just wrapped it with IO, so you made your computer run CPU even though it eventually wasn't necessary.

    With pure computations it might look like an optimization, but what if you had side effects there?

    def dropAllUsersInDB: Future[Unit]
    
    def eagerDrop = IO.fromFuture(IO.pure(dropAllUsersInDB))
    
    def lazyDrop = IO.fromFuture(IO.suspend(IO.pure(dropAllUsersInDB)))
    

    The first example creates Future which would drop all users - so whether or not we compose this IO into our program Users are being deleted.

    The second example have suspend somewhere in the IO recipe so this Future is NOT created unless IO will be evaluated. So Users are safe unless we explicitly compose this IO into something that will become a part of the computation.

    In your example this will become more visible if you do:

    def fib(n: Int, a: Long, b: Long): IO[Long] =
      IO.suspend {
        println("evaluating")
        if (n > 0)
          fib(n - 1, b, a + b)
        else
          IO.pure(a)
      }
    

    and

    @tailrec
    def fib(n: Int, a: Long, b: Long): IO[Long] = {
      println("evaluating")
      if (n > 0)
        fib(n -1, b, a + b)
      else
        IO.pure(a)
    }
    

    Then call fib to create an IO value without evaluating it, and you'll see the difference.