Conversation
Order[Double].compare used java.lang.Double.compare, which distinguishes signed zeros, while eqv used primitive ==. OptionOrder.eqv derives from compare, so Option(0.0) === Option(-0.0) was false despite 0.0 === -0.0. Align compare with IEEE floating equality (+0 equals -0) and fall back to Double/Float.compare only for NaN total ordering. Canonicalize zero in hash so Hash agrees with eqv. Fixes typelevel#4807
johnynek
left a comment
There was a problem hiding this comment.
I think we should merge this.
Thank you
|
I think the native CI flaked. Can you merge origin/main and push to the branch. Let's see if CI passes then. |
|
Merged |
|
@satorg what do you think? Can we click merge? |
| if (x < y) -1 | ||
| else if (x > y) 1 | ||
| else if (x == y) 0 | ||
| else java.lang.Double.compare(x, y) |
There was a problem hiding this comment.
The first thing that Double.compare does is this:
public static int compare(double d1, double d2) {
if (d1 < d2)
return -1; // Neither val is NaN, thisVal is smaller
if (d1 > d2)
return 1; // Neither val is NaN, thisVal is larger
...Is there a reason for repeating those < and > comparisons in our code?
How about this:
def compare(x: Double, y: Double): Int =
if (x == y) 0
else java.lang.Double.compare(x, y)| } | ||
|
|
||
| // #4807: +0.0 and -0.0 must agree across eqv, compare, and derived Option eqv | ||
| test("#4807 Double signed zeros are equal under Order and Option") { |
There was a problem hiding this comment.
The tests are pretty comprehensive, thank you!
However, I'd suggest several improvements, because:
- Tests for
DoubeandFloatare identical. - The tests always have
0.0as LHS and-0.0as RHS. However, we probably should be checking-0.0vs0.0too in order to prevent asymmetry.
That suggests that the checks could probably be extracted into a generic function and then run for
(0.0, -0.0)(-0.0, 0.0)(0.0f, -0.0f)(-0.0f, 0.0f)
Also, it probably makes sense to split the tests according the type classes being tested:
Orderchecks should stay inOrderSuiteHashchecks inHashSuiteEqchecks inEqSuite.
I realize that this specific PR is focused on the specific issue, but long-term it may be confusing to see tests for Hash or Eq here in OrderSuite.
There was a problem hiding this comment.
Also, I’m not sure it’s generally a good idea to include the issue number in the test name. Test names should clearly describe what is being tested, rather than the issue that prompted the PR. If cross-referencing is useful (which it probably is) – adding the issue number to a code comment should be sufficient.
Summary
Fixes #4807.
Order[Double].eqv/Order[Float].eqvuse primitive==, so positive and negative zero compare equal:comparepreviously delegated tojava.lang.Double.compare/Float.compare, which implement a total order that distinguishes signed zeros.OptionOrder.eqvis derived fromcompare(compare == 0), so:That disagreed with both bare
===and Scala'sOption(0.0) == Option(-0.0).Change
DoubleOrder/FloatOrder.compare: use</>/==so+0and-0are equal (matchingeqvand IEEE floating equality). Fall back tojava.lang.*.compareonly when both relational and equality checks fail (NaN total ordering).hash: canonicalize±0to+0before hashing soHashagrees witheqvfor zeros (bit-pattern hashes previously differed).Alternative considered
Making
eqvuseDouble.compare(x, y) == 0would also restorecompare/eqvconsistency, but would make0.0 === -0.0false and would makeNaN === NaNtrue — a larger break from primitive==and from the behavior reported in #4807.Tests
testsJVM/testOnly cats.tests.OrderSuite— 91/91 (Corretto 8 / Scala 2.13), including#4807cases for Double and Float (bare===,Order.compare/eqv,Hash, andOption===/Order).Checklist
Fixes #4807)