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
    No fields configured for issues without a type.

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions