Sunday, February 08, 2015

Getting rid of compareTo for ==

NOTE:This is article is thought as prelude to a discussion on the mailing list about a possible removal of the general compareTo path for the equality operator.

As many may know Groovy has quite the complicated logic for the == operator. Which is to call equals unless the left side implements Comparable, in which case we use compareTo... well simplified...

To illustrate the logic:

 class A implements Comparable {
boolean equals(Object o) {false}
int compareTo(Object o) {0}
def xa = new A()
def ya = new A()
assert xa==xa && ya==ya // referential identity override
assert !xa.equals(ya) // direct call to equals
assert xa.compareTo(ya)==0 // direct call to compareTo
assert xa==ya // ignores equals, since it implements Comparable

class B implements Comparable {
boolean equals(Object o) {false}
int compareTo(Object o) {0}
def xb = new B()
assert !xa.equals(xb) && !xb.equals(xa)
assert xa.compareTo(xb)==0 && xb.compareTo(xa)==0
assert xa!=xb // ignores equals as well as Comparable

assert 1==1l // compare primtive long and int
assert !(1.0G.equals(1.00G))
assert 1.0G==1.00G // compare BigDecimals with differing scale
assert 1.0G==1l // compare primitive long with BigDecimal
assert 1G==1.0G // compare BigInteger and BigDecimal
assert 1!=new Object() // compare primitive with incompatible instances

In Java you know that this operator allows you for example to compare ints and longs and does this in the given case by transforming the int into a long to then compare the numeric values. Similar things happen for the other primitives. Since Java5 the operator does even allow you to compare a primitive int and an Integer by using autoboxing. Where the equality operator in Java fails is if you compare for example a Long and an Integer. Fail in the sense that it does not the same as for the primitive counter parts.

Now in Groovy the equality operator traditionally had to handle comparing the boxed versions as if they are not boxed. This is because in versions of Groovy before 1.8 every primitive declared variable actually used the boxed version. Only in 1.8 I introduced actual primitives, but the ability of the equality operator to compare for example Integer and Long stayed. It had to stay, because we don't only compare those, we have also those 1-char Strings, that are supposed to be equal to Strings, GString logic and of course BigInteger and BigDecimal logics.

BigDecimal now does something that is not really advised when implementing the interface Comparable, it returns false using equals for a case that is seen as equal for compareTo. For example "1.0" and "1.00" is such a case. They are not equal, because the scale is not, even if the value is projectable without precision loss to the other to do an actual compare.

Since people do also things like `1==new Object()` and since this is not supposed to throw a ClassCastExpcetion, even though the compareTo method will do that here, we had also to add a special logic doing the compareTo call only, iff the right side type is a subtype of the left side type.

This causes all kinds of confusion to people. And my suggestion is to remove the compareTo path.

Instead I suggest adding a path special to BigDecimal to handle the equals problem. This should remove a lot of confusion in the future.

Now this will of course have more impact than some people may think. Obviously classes implementing Comparable may now behave different. But especially custom Number implementations may do that now. So it is a loss of feature to some extend. But if the usage of those features is causing more problems than abilities it allows, then we have to rethink this. And I think this is the case here. My intended change would also change the behavior of the program above. The referential equality override would stay, but "assert xa==ya" would then fail, since equals returns always false. Also if equals did return always true "assert xa!=xb" would fail, since before it did not call equals and now does.

No comments: