jakstack
jakstack

Reputation: 2205

Use Cats Effect Ref as a cache

Trying to implement caching the functional way using Cats Effect Ref monad.

Why is the internal Ref not be set as expected?

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

object SomeMain extends IOApp.Simple {

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

  override def run: IO[Unit] = {

    val checkValueBeforeSet = cache.flatMap(ref => ref.get.flatMap {
      case Some(v) => IO(println(v))
      case None => IO(println("as expected no value yet"))
    })

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

    val checkValueAfterSet = cache.flatMap(ref => ref.get.flatMap {
      case Some(v) => IO(println(v))
      case None => IO(println("unexpected still no value set!"))
    })

    for {
      _ <- checkValueBeforeSet
      _ <- doSetAction
      _ <- checkValueAfterSet
    } yield IO()
  }
}

Output:

as expected no value yet
set action done
unexpected still no value set!

Upvotes: 1

Views: 315

Answers (1)

Mateusz Kubuszok
Mateusz Kubuszok

Reputation: 27535

It doesn't work because you are creating Ref anew each time.

This:

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

is the same as:

val cache: () => AtomicReference[Option[String]] =
  () => new AtomicReference(None)

(If you don't understand the semantics of the IO[A] you can always imagine it is () => A or () => Future[A] and then it makes sense).

You are using cache twice (thrice) inside some helper method and these methods are creating local Ref and then let it be forgotten.

You'd have to keep the value returned by cache and pass it (w.g. with dependency injection) to make this work:

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

object SomeMain extends IOApp.Simple {

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

  override def run: IO[Unit] = cache.flatMap { ref =>

    val checkValueBeforeSet = ref.get.flatMap {
      case Some(v) => IO(println(v))
      case None => IO(println("as expected no value yet"))
    }

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

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

    for {
      _ <- checkValueBeforeSet
      _ <- doSetAction
      _ <- checkValueAfterSet
    } yield IO()
  }
}

Basically, Ref is a wrapper around AtomicReference which runs each operation in an IO. It let mutate state safely but it's creation is unsafe on its own (IO[Ref[]] creates new Ref each time you compose it into the program), so you have to pay attention how you composing it into the program.

Alternatively, you can use .memoize, or create Ref with some unsafe method, but it's a great way of shooting yourself in a foot in a long run, if you don't have a very good intuition what you are doing.

Upvotes: 6

Related Questions