Thanks to visit codestin.com
Credit goes to github.com

Skip to content

Conversation

@eyalfa
Copy link
Contributor

@eyalfa eyalfa commented Oct 22, 2024

found a couple of bugs in Cause.filter(..) while hacking on something completely different...
fix seemed quite easy, so here goes...

def bothCase(context: Any, left: Cause[E], right: Cause[E]): Cause[E] =
if (p(left)) {
if (p(right)) Cause.Both(left, right)
if (left.nonEmpty) {
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

What's the rationale behind this change? Shouldn't we use the predicate when deciding if we're going to include left or right?

Copy link
Contributor Author

@eyalfa eyalfa Oct 22, 2024

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

left and right are already filtered at this point

Copy link
Contributor

@kyri-petrou kyri-petrou Oct 29, 2024

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I think the fact that are already filtered is because of how bothCase and thenCase are used currently. I would be more comfortable keeping p(c) instead of assuming they're going to be filtered by the time we call these methods.

Copy link
Contributor Author

@eyalfa eyalfa Oct 29, 2024

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I actually think it's guaranteed by the fold semantics, furthermore applying p(c) won't be any cheaper thatn testing for emptiness.

Copy link
Contributor

@kyri-petrou kyri-petrou Oct 29, 2024

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

My point is that while it's guaranteed by the fold semantics, what's not guaranteed is that the bothCase and thenCase methods might be used in a method other than fold. Since Folder.Filter is a public class (not sure why, but we can't ignore that it is), this test will fail using the implementation in this PR:

          test("filter(false)") {
            val filter = Cause.Folder.Filter[Unit](_ => false)
            val cause = filter.bothCase((), f1, f2)
            assertTrue(cause.isEmpty)
          }

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I'm also not sure why the Filter class exists as a class instead of a fold call or anonymous class, nevertheless seems like it's being public is simply a miss-sight.
anyway, as a Folder it's intended to be used by folds, I guess almost any other Folder implementation can be abused in similar ways to your example... what matters here is the semantics of filtering a Cause which are currently broken and have to be fixed, I think modifying the existing code is the path of least resistance, if you disagree we can always deprecate the exiting class and create a new one or simply replace it with a private/anonymous one.

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I think for now let's go with using p(c) instead of nonEmpty in bothCase and thenCase. I'm planning on looking into the performance of error paths in the future (including Cause), so maybe add a TODO comment so that we don't forget it

@eyalfa eyalfa requested a review from kyri-petrou October 23, 2024 11:50
Copy link
Contributor

@kyri-petrou kyri-petrou left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

By the way do we need to also modify the stacklessCase method to be aligned with failCase etc?

def bothCase(context: Any, left: Cause[E], right: Cause[E]): Cause[E] =
if (p(left)) {
if (p(right)) Cause.Both(left, right)
if (left.nonEmpty) {
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I think for now let's go with using p(c) instead of nonEmpty in bothCase and thenCase. I'm planning on looking into the performance of error paths in the future (including Cause), so maybe add a TODO comment so that we don't forget it

@eyalfa
Copy link
Contributor Author

eyalfa commented Nov 4, 2024

@kyri-petrou
I think replacing nonEmpty with p(c) is a beaking change due to multiple reasons:

  1. the original code never applied p over a Then or a Both, doing so may introduce regressions.
  2. it's quite possible these applications of p may fail with runtime match errors. (I suspect they are implemented using pattern matching).

@kyri-petrou
Copy link
Contributor

the original code never applied p over a Then or a Both, doing so may introduce regressions.

@eyalfa I'm not sure I understand your comment. This is the current code for thenCase which uses p. Am I misunderstanding something?

      def thenCase(context: Any, left: Cause[E], right: Cause[E]): Cause[E] =
        if (p(left)) {
          if (p(right)) Cause.Then(left, right)
          else left
        } else if (p(right)) right
        else Cause.empty

@eyalfaZS
Copy link

eyalfaZS commented Nov 6, 2024

the original code never applied p over a Then or a Both, doing so may introduce regressions.

@eyalfa I'm not sure I understand your comment. This is the current code for thenCase which uses p. Am I misunderstanding something?

      def thenCase(context: Any, left: Cause[E], right: Cause[E]): Cause[E] =
        if (p(left)) {
          if (p(right)) Cause.Then(left, right)
          else left
        } else if (p(right)) right
        else Cause.empty

it never applies p on the Then instance itself, it only considers it for composition.

@kyri-petrou
Copy link
Contributor

it never applies p on the Then instance itself, it only considers it for composition.

@eyalfa I'm not suggesting we should apply p on the Then instance itself, just p(left) and p(right) instead of left.nonEmpty and right.nonEmpty in this PR

@eyalfaZS
Copy link

eyalfaZS commented Nov 7, 2024

the original code never applied p over a Then or a Both, doing so may introduce regressions.

@eyalfa I'm not sure I understand your comment. This is the current code for thenCase which uses p. Am I misunderstanding something?

      def thenCase(context: Any, left: Cause[E], right: Cause[E]): Cause[E] =
        if (p(left)) {
          if (p(right)) Cause.Then(left, right)
          else left
        } else if (p(right)) right
        else Cause.empty

it would in the case of a composite Then (when one of the side is also a Then)

@jdegoes
Copy link
Member

jdegoes commented Nov 8, 2024

@eyalfa Would love to get this fix in. Looks like @kyri-petrou suggested a minor tweak, and then looks good to merge!

@eyalfaZS
Copy link

eyalfaZS commented Nov 9, 2024

@jdegoes , @kyri-petrou ,
sorry for being less responsive the past few days, I've taken a new position and last few days were packed.

I think Kyrie's suggestion changes the semantics of the filter in such a way that it may break existing code.
original implementation applied p only on the 'leafs', never on the Both and Then cases (Stackless neither), Kyrie's suggestion does that, i.e. when we have something like Both(c0, Both(c1, c2)) p would be applied on the nested Both as well which is likely to fail if p is implemented as a partial function.

looking at the original code it's quite clear that Both, Then and Stackless are treated as containers/collections of causes and are never filtered on their own. I think a better approach is to replaced filtered out causes with a Cause.Empty and pattern match when composing - which is very similar to what the current code is doing (with a bug 😎 ).

  • another issue I see with this operator is losing annotations, seems like there are two 'levels' of implementing a Fold and for some reason Filter chose the one losing annotations.

@CLAassistant
Copy link

CLAassistant commented Nov 9, 2024

CLA assistant check
All committers have signed the CLA.

@eyalfa
Copy link
Contributor Author

eyalfa commented Nov 9, 2024

@kyri-petrou I think this is the best way to go, if you're still concerned about anyone using this class directly we can always deprecate it and replace it with a new one or just use the fold override taking lambdas

@kyri-petrou
Copy link
Contributor

kyri-petrou commented Nov 11, 2024

I think Kyrie's suggestion changes the semantics of the filter in such a way that it may break existing code.
original implementation applied p only on the 'leafs', never on the Both and Then cases (Stackless neither)

@eyalfa This is not true. In fact, what's changing the semantics of filter is the new implementation. I managed to create a reproducer that showcases a change in behaviour with your implementation:

object App {
  val c1   = Cause.fail("foo")
  val c2   = Cause.fail("bar")
  val c3   = Cause.fail("baz")
  val c23  = Cause.Both(c2, c3)
  val c123 = Cause.Both(c1, c23)

  def main(args: Array[String]): Unit = {
    val c = c123.filter { c =>
      println(s"applying filter on: ${c.failures}")
      !c.isInstanceOf[Cause.Both[?]]
    }

    println(s"\nfiltered cause: $c")
  }
}

This code yields the following outputs:

series/2.x:

applying filter on: List(bar)
applying filter on: List(baz)
applying filter on: List(foo)
applying filter on: List(bar, baz)

filtered cause: Fail(foo,Stack trace for thread "zio-fiber-":
)

PR (current bothCase implementation)

applying filter on: List(foo)
applying filter on: List(bar)
applying filter on: List(baz)

filtered cause: Both(Fail(foo,Stack trace for thread "zio-fiber-":
),Both(Fail(bar,Stack trace for thread "zio-fiber-":
),Fail(baz,Stack trace for thread "zio-fiber-":
)))

PR (/w my suggestion)

applying filter on: List(foo)
applying filter on: List(bar)
applying filter on: List(baz)
applying filter on: List(bar)
applying filter on: List(baz)
applying filter on: List(foo)
applying filter on: List(bar, baz)

filtered cause: Fail(foo,Stack trace for thread "zio-fiber-":
)

As you can see, both the current code (series/2.x) and my suggestion apply the filter on Both, but that's not the case with your proposed changes.

Having said that, the downside of my suggestion is that it ends up evaluating p(c) on same Cause multiple times, which is suboptimal. There are ways to improve performance in this case, but since I find it extremely difficult to picture Cause#filter being used in CPU hotpaths, I think we can ignore the performance penalty for now. In the future I plan to visit Cause methods and optimize them as much as possible

@eyalfa
Copy link
Contributor Author

eyalfa commented Nov 11, 2024

@kyri-petrou ,
I actually understood that the original impl also applies p to Both and Then in the nested case, unfortunately I wrote my comment before this understanding hitting me 😎 .

I think at this point it's a matter of deciding what's the correct behavior, I suspect it's applying p on the Both case only when one is actually constructed, but tbh I'm not 100% sure.

another thing is, I wouldn't expect filter to apply the predicate more than once per Cause, original impl has this property. but it includes a bug for the 'single' case.

I think the logic for Both should be something like:
if both sides are filtered out return Empty
if exactly one side is filtered out, return the other
otherwise, construct a Both, apply the predicate on the new instance, in case of false return Empty otherwise return the new instance.

sketched the implementation and got these printouts when running your sample:

applying filter on: List(foo)
applying filter on: List(bar)
applying filter on: List(baz)
applying filter on: List(bar, baz)

filtered cause: Fail(foo,Stack trace for thread "zio-fiber-":
)

I think this approach yields the correct results while maintaining the 'apply once' property.

@eyalfaZS
Copy link

@kyri-petrou ☝️

else left
} else if (p(right)) right
else Cause.empty
(left, right) match {
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Let's avoid the intermediate tuple creation here and in the methods below and use simple if (left eq Cause.Empty) { ... } else if (right eq Cause.Empty) { ... } else { ... } statements

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

done

@kyri-petrou kyri-petrou merged commit 252e23e into zio:series/2.x Nov 28, 2024
18 checks passed
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

5 participants