-
Couldn't load subscription status.
- Fork 1.4k
Cause filter fix #9254
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Cause filter fix #9254
Conversation
| 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) { |
There was a problem hiding this comment.
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?
There was a problem hiding this comment.
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
There was a problem hiding this comment.
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.
There was a problem hiding this comment.
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.
There was a problem hiding this comment.
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)
}There was a problem hiding this comment.
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.
There was a problem hiding this comment.
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
There was a problem hiding this 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) { |
There was a problem hiding this comment.
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
|
@kyri-petrou
|
@eyalfa I'm not sure I understand your comment. This is the current code for 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 |
@eyalfa I'm not suggesting we should apply |
it would in the case of a composite |
|
@eyalfa Would love to get this fix in. Looks like @kyri-petrou suggested a minor tweak, and then looks good to merge! |
|
@jdegoes , @kyri-petrou , I think Kyrie's suggestion changes the semantics of the looking at the original code it's quite clear that
|
58a0a04 to
41c17c6
Compare
41c17c6 to
9e2c66c
Compare
|
@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 |
@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: PR (current PR (/w my suggestion) As you can see, both the current code (series/2.x) and my suggestion apply the filter on 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 |
|
@kyri-petrou , I think at this point it's a matter of deciding what's the correct behavior, I suspect it's applying p on the another thing is, I wouldn't expect filter to apply the predicate more than once per I think the logic for sketched the implementation and got these printouts when running your sample: I think this approach yields the correct results while maintaining the 'apply once' property. |
|
@kyri-petrou ☝️ |
| else left | ||
| } else if (p(right)) right | ||
| else Cause.empty | ||
| (left, right) match { |
There was a problem hiding this comment.
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
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
done
found a couple of bugs in
Cause.filter(..)while hacking on something completely different...fix seemed quite easy, so here goes...