Created
January 22, 2024 20:39
-
-
Save nzpr/38f843139224d1de1f0e9d15c75e925c to your computer and use it in GitHub Desktop.
Bug in Applicative implementation for Eval
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
import cats.effect.IO | |
import cats.effect.unsafe.implicits.global | |
import cats.syntax.all.* | |
import cats.{Applicative, Eval} | |
import org.scalatest.flatspec.AnyFlatSpec | |
import org.scalatest.matchers.should.Matchers | |
class EvalApplicativeBug extends AnyFlatSpec with Matchers { | |
// Number of traversal iterations | |
val n = 10 | |
// List to traverse, 10 iterations | |
val tries: List[Int] = (0 until n).toList | |
// 4 ways to traverse list executing effectful function | |
// this works, use .as to make sure Unit is return type | |
def ok1[F[_]: Applicative](f: F[Unit]): F[Unit] = tries.traverse(_ => f).as(()) | |
// this works, make return type List[Unit] | |
def ok2[F[_]: Applicative](f: F[Unit]): F[List[Unit]] = tries.traverse(_ => f) | |
// this does not work, use .void to make sure Unit is return type | |
def err1[F[_]: Applicative](f: F[Unit]): F[Unit] = tries.traverse(_ => f).void | |
// this does not work, use .traverse_ to make sure Unit is return type | |
def err2[F[_]: Applicative](f: F[Unit]): F[Unit] = tries.traverse_(_ => f) | |
"Traversal using Applicative for Eval" should "work" in { | |
// Mutable var to count how many times effect is executed. | |
var x = 0 | |
// The effect. Function to increase x, lazy without caching. Calling this should increase x by 1. | |
def f: Eval[Unit] = Eval.always(x += 1) | |
x = 0 | |
ok1(f).value | |
val xOk1 = x | |
x = 0 | |
ok2(f).value | |
val xOk2 = x | |
x = 0 | |
err1(f).value | |
val xFail1 = x | |
x = 0 | |
err2(f).value | |
val xFail2 = x | |
// This is the actual outcome | |
List(xOk1, xOk2, xFail1, xFail2) shouldBe List(n, n, 0, 0) | |
// Expected outcome is to all these calls to be the same. But this is not the case. | |
List(xOk1, xOk2, xFail1, xFail2) shouldBe List(n, n, n, n) | |
} | |
"Traversal using Applicative for IO" should "work" in { | |
// Mutable var to count how many times effect is executed. | |
var x = 0 | |
// The effect | |
def f: IO[Unit] = IO.delay(x += 1) | |
x = 0 | |
ok1(f).unsafeRunSync() | |
val xOk1 = x | |
x = 0 | |
ok2(f).unsafeRunSync() | |
val xOk2 = x | |
x = 0 | |
err1(f).unsafeRunSync() | |
val xFail1 = x | |
x = 0 | |
err2(f).unsafeRunSync() | |
val xFail2 = x | |
// Expected outcome is to all these calls to be the same. | |
List(xOk1, xOk2, xFail1, xFail2) shouldBe List(n, n, n, n) | |
} | |
} |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
This turned out to be not a bug but a feature of Eval, which is not designed to suspend side-effects.
typelevel/cats#4553