Skip to content

Inconsistent behaviour when using Eval as Applicative #4552

@nzpr

Description

@nzpr

I encountered odd behaviour when using Eval as Applicative to traverse when doing stack safe serialization / deserialization.
I managed to see that these syntaxes behave differently.

.traverse(_ => f).as(())
.traverse(_ => f)
.traverse(_ => f).void
.traverse_(_ => f)

I would expect all these execute f in exactly the same way, but it's not. Fo IO it works as expected.
Here is the code https://scastie.scala-lang.org/nzpr/UF5cug4wSYSeFurF434r9w/1

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)

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Type

    No type

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions