Skip to content

Instantly share code, notes, and snippets.

@nzpr
Created January 22, 2024 20:39
Show Gist options
  • Save nzpr/38f843139224d1de1f0e9d15c75e925c to your computer and use it in GitHub Desktop.
Save nzpr/38f843139224d1de1f0e9d15c75e925c to your computer and use it in GitHub Desktop.
Bug in Applicative implementation for Eval
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)
}
}
@nzpr
Copy link
Author

nzpr commented Feb 9, 2024

This turned out to be not a bug but a feature of Eval, which is not designed to suspend side-effects.
typelevel/cats#4553

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment