Reputation: 2205
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
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