Inconsistent behaviour when using Eval as Applicative #4553
-
I encountered odd behaviour when using Eval as Applicative to traverse when doing stack safe serialization / deserialization. .traverse(_ => f).as(())
.traverse(_ => f)
.traverse(_ => f).void
.traverse_(_ => f) I would expect all these execute import cats.effect.IO
import cats.effect.unsafe.implicits.global
import cats.syntax.all._
import cats.{Applicative, Eval}
val traversable: List[Int] = (0 until 10).toList
// 4 ways to traverse list
// this works for Eval, use .as to make sure Unit is return type
def ok1[F[_]: Applicative](f: F[Unit]): F[Unit] = traversable.traverse(_ => f).as(())
// this works for Eval, make return type List[Unit]
def ok2[F[_]: Applicative](f: F[Unit]): F[List[Unit]] = traversable.traverse(_ => f)
// this does not work for Eval, use .void to make sure Unit is return type
def err1[F[_]: Applicative](f: F[Unit]): F[Unit] = traversable.traverse(_ => f).void
// this does not work for Eval, use .traverse_ to make sure Unit is return type
def err2[F[_]: Applicative](f: F[Unit]): F[Unit] = traversable.traverse_(_ => f)
// Mutable var to count how many times effect is executed.
var x = 0
// The effect. Calling these should increase x by 1.
def fEval: Eval[Unit] = Eval.always(x += 1) // Eval effect, this is what misbehaves
def fIO: IO[Unit] = IO.delay(x += 1) // IO effect (for comparison)
// Traverse using Eval
x = 0
ok1(fEval).value
val eval1 = x
x = 0
ok2(fEval).value
val eval2 = x
x = 0
err1(fEval).value
val eval3 = x
x = 0
err2(fEval).value
val eval4 = x
// Traverse using IO (for comparison)
x = 0
ok1(fIO).unsafeRunSync()
val io1 = x
x = 0
ok2(fIO).unsafeRunSync()
val io2 = x
x = 0
err1(fIO).unsafeRunSync()
val io3 = x
x = 0
err2(fIO).unsafeRunSync()
val io4 = x
println(List(eval1, eval2, eval3, eval4))
// List(10, 10, 0, 0)
println(List(io1, io2, io3, io4))
// List(10, 10, 10, 10)
// So with Eval effects are not executed for .traverse(_ => f).void and .traverse_(_ => f) |
Beta Was this translation helpful? Give feedback.
Replies: 1 comment 1 reply
-
|
Beta Was this translation helpful? Give feedback.
Eval
is for stack-safety of pure computation. UnlikeIO
,Eval
is not for suspending side-effects. So if you are discarding the result ofEval
e.g. withvoid
then there is no purpose to run that computation. Because the computation should be pure and have no side-effects you would not notice that it didn't run. So it is an optimization to not run it.