scalafunctional-programmingcats-effect

Use Cats Effect Ref as a cache - Part 2


Part 1

The reason the value set in first-run isn't seen in the second-run and hence getting the message "strange! no value found in secondRun" is because every time I call:

cache.flatMap { ref

I'm getting a different 'ref' instance

But why?

Is there way to fix/do this so that second-run would find the value set in first-run?

import cats.effect.kernel.Ref
import cats.effect.{IO, IOApp}

object SomeMain2 extends IOApp.Simple {

  val cache: IO[Ref[IO, Option[String]]] = Ref.of[IO, Option[String]](None)

  override def run: IO[Unit] = {
    for {
      _ <- firstRun
      _ <- secondRun
    } yield IO()
  }

  private def firstRun: IO[Unit] = cache.flatMap { ref =>

    val checkValueBeforeSet = ref.get.flatMap {
      case Some(v) => IO(println(v))
      case None => IO(println("no value found in firstRun"))
    }

    val doSetAction = ref.set(Some("abc")).map(_ => println("set action done in firstRun"))

    val checkValueAfterSet = ref.get.flatMap {
      case Some(v) => IO(println(s"as expected value found: $v"))
      case None => IO(println("unexpected still no value set!"))
    }

    for {
      _ <- checkValueBeforeSet
      _ <- doSetAction
      _ <- checkValueAfterSet
      _ <- IO(println(s"ref: $ref"))
    } yield IO()
  }

  private def secondRun: IO[Unit] = cache.flatMap { ref =>

    val checkValue = ref.get.flatMap {
      case Some(v) => IO(println(v))
      case None => IO(println("strange! no value found in secondRun"))
    }

    for {
      _ <- checkValue
      _ <- IO(println(s"ref: $ref"))
    } yield IO()
  }
}

Output:

no value found in firstRun
set action done in firstRun
as expected value found: abc
ref: cats.effect.kernel.SyncRef@14dfe50a
strange! no value found in secondRun
ref: cats.effect.kernel.SyncRef@1bca3dbe

Solution

  • This is the same issue as in part 1 but under a different form. The answer from Mateusz in the 1st question still applies here. Let me adapt it for you in this situation.

    You need to reuse the same Ref instance in all your program(s):

    val cache: IO[Ref[IO, Option[String]]] = Ref.of[IO, Option[String]](None)
    
    override def run: IO[Unit] = {
      for {
        cacheRef <- cache
        _ <- firstRun(cacheRef)
        _ <- secondRun(cacheRef)
      } yield IO()
    }
    
    private def firstRun(cacheRef: Ref[...]): IO[Unit] = {
      // do something with cacheRef
    }
    
    private def secondRun(cacheRef: Ref[...]): IO[Unit] = {
      // do something with cacheRef
    }
    

    Edit: this is a very basic approach though. In a real-life application, you would likely use dependency injection and hide the Ref instance in a shared service responsible to manage it. (Thanks Mateusz for pointing that out in the comments).