-
Notifications
You must be signed in to change notification settings - Fork 3.1k
bug#10511 Make Ordering methods consistent #6323
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
Conversation
/sync |
see also scala/bug#5104 this is the kind of thing it can be hard to get a reviewer for... I'm somewhat uncomfortable with losing the ScalaCheck tests that were added in 460bbc1, are any of them still applicable, or easily adapted to be applicable? @khernyo are you still around to weigh in...? |
@SethTisue all of the ScalaCheck tests check that various operations are inconsistent with |
The root problem is that IEEE floating point values do not have a total ordering; only a partial ordering. Thus, To complicate matters further, Since 460bbc1, A possible solution is to use Another solution is to change |
0acffd0
to
b7915be
Compare
Remove overridden method implementations in Ordering. Reverts 460bbc1 Fixes scala/bug#10511
b7915be
to
3048e24
Compare
I just skimmed it for now but as I see it, you are trading one bugfix for another. I can understand where you are coming from, but accepting this MR will break currently working code, so let's not do that. |
Sorry for not responding sooner. I'm afraid I'm not the best person to review this. (I like floats, but don't know much about comparing them :-)) |
No worries. To be honest, I'm not sure who's the best person to review it. Edit: Sorry the bot added you back @adriaanm - I've removed the original comment, and hopefully it won't do that again |
@khernyo can I ask what currently working code it breaks? |
@NthPortal, the commit you reverted is about consistency of primitive relational operators and Ordering of floats. So, any code that were counting on these behaving the same might break. I'd say that 460bbc1 was a short sighted fix but the consistency it provided was good to have. The fixes in the current MR are also desired, but as you concluded, the two fixes are incompatible. I have no idea which one is more important to have, so it's fine by me either way. I'd vote for using PartialOrdering instead of Ordering for floats but I know it's a huge undertaking. AFAIK, all other solutions will end up with inconsistencies. |
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.
There is a fundamental problem with ordering floating-point values, as people have already pointed out. However, this commit doesn't, I think, meaningfully improve the situation because it breaks as much as it fixes.
The fundamental problem is in the IEEE spec, but we still should try to make things work as naturally as possible. In particular, there are three really natural identities, at least one of which we will have to break.
def invariants(x: Double, y: Double) = {
val o = implicitly[Ordering[Double]]
def bits(x: Double) =
java.lang.Double.doubleToRawLongBits(x)
(x < y) == O.lt(x, y) && // #1
!(O.lt(x, y) && !bits(O.min(x, y)) == bits(x)) && // #2
O.min(x, y) == (x min y) // #3
}
The current state of affairs is that #1 and #3 are true but #2 is false; this PR swaps it so that #1 and #3 are false but #2 is true (but has the advantage of a simpler implementation).
There is an additional constraint, which is orderings should be usable for sorting things. Unfortunately, #1 is incompatible with this; it fails the basic invariant that a total ordering must have of !(x < y && y < x)
. Sorts often assume this is true, and breaking it can cause not just mis-sorting but runtime failures in the algorithm. Unfortunately, the current state of affairs is that the ordering isn't an ordering.
If we want an ordering, we have to abandon #1, but arguably <
has pretty batty behavior anyway for an operator named that.
We still can choose, though, whether #2 or #3 is true. And here, I think, the resolution falls in favor of #3 being true.
This still leaves us with two possibilities regarding the behavior of min
. And here I think it's very dangerous to switch from min
returning NaN
to it returning a value in any case where you might stumble into it by accident. (This could be literally dangerous if anyone relies upon the behavior to detect an error condition.) The most problematic place this could occur subtly is in something like List(1.0, 2.0, Double.NaN).min
.
It is also possible to have a typeclass that is weaker that Ordering
which is something like Extrema
which just defines min
and max
methods which is implemented by Ordering
but for which there's a higher precedence implicit for Extrema[Double]
and Extrema[Float]
so that collections min/max can work the "right way". Failing that, I would say that overriding min
and max
in FloatOrdering
and DoubleOrdering
is needed in this PR for it to be considered (it's always more dangerous to go from an error-signaling to an error-ignoring behavior).
I'm still not sure that this PR is an improvement even with that change. It might be better to simply add a separate total ordering that users can apply when they know they want the total ordering.
The problem is, My concern isn't that methods in The purpose of this PR is to make it so that I could change the PR so that Regarding #1, I don't believe there are any values for which |
If others are amenable to it, I would be happy to work on the significant change of having I don't know how much code this would break in the community. We could potentially add |
@NthPortal - Right now, #3 is not violated: Welcome to Scala 2.12.4 (OpenJDK 64-Bit Server VM, Java 1.8.0_151).
Type in expressions for evaluation. Or try :help.
scala> val o = implicitly[Ordering[Double]]
o: Ordering[Double] = scala.math.Ordering$Double$@5699578f
scala> o.min(1.0, Double.NaN)
res0: Double = NaN
scala> (1.0 min Double.NaN)
res1: Double = NaN However, the inconsistency you point out is a problem. But I think the resolution is that
rather than breaking
Both are surprising, but the latter seems even more surprising to me. Anyway, I am not fine with Note that you can't actually have the consistency you wanted if you follow IEEE rules, because IEEE rules are that everything is false. But of course we want So I think the marginally sensible possibilities are
None of these are great, but I maintain that everything else is worse. Again, I don't think it's acceptable that |
Incidentally, I don't think |
@Ichoran ah, but Edit: perhaps that should be changed, but it is the current behaviour |
@NthPortal - Yes, but |
@Ichoran Why doesn't trait DoublePartialOrdering extends PartialOrdering[Double] {
def tryCompare(x: Double, y: Double): Option[Int] = {
if (x < y) Some(-1)
else if (x > y) Some(1)
else if (x == y) Some(0) // includes -0.0 == 0.0
else None // one or both is NaN
}
def lteq(x: Double, y: Double): Boolean = x <= y
override def gteq(x: Double, y: Double): Boolean = x >= y
override def lt(x: Double, y: Double): Boolean = x < y
override def gt(x: Double, y: Double): Boolean = x > y
override def equiv(x: Double, y: Double): Boolean = x == y
} And if you don't use |
My mistake-- |
If we have |
Does that resolve the max/min problem? |
I think so? It makes it so that As you mentioned, it would involve adding an |
So the proposal is to change the behavior of |
Correct; I am proposing that it would return If you wanted both |
I think the But I guess there is an issue with sorted sets, then, as the trivial implementation wouldn't work. I do think we should do something; I just don't think this reversion is enough to get there. |
I have some other issues with Edit: your concerns are another good reason to start a thread |
Funnily enough, I've looked at sorted sets, and there's no easy way to get the min/max by the set's |
Do you think it's worth going to all the trouble of changing the ordering type, adding new traits, and so on? Or do you think it's maybe better to define a total ordering in, say, |
I do think it's worth it. Though adding a I've started working on a rewrite/redesign over at #6375 |
Suppose we change If it returns a (It shouldn't be called regardless, but still) |
It has to return |
Hopefully no one calls |
@Ichoran I just realized a major problem with using the current |
@NthPortal - Good point. However, this can be fixed by making sure that |
@Ichoran true (and I believe that's what they already do); it's still a possible pitfall though |
I agree with the suggestion you made on Discourse, though I'm not certain the best way to implement it |
@NthPortal - Just figure out wherever |
superseded by #6410 |
Remove overridden method implementations in Ordering.
Reverts 460bbc1
Fixes scala/bug#10511