Why Kotlin doesn't respect comparable properties in equals() method during bytecode generation for dataclasses?


#1

Hello, my question related to Kotlin bytecode generation for dataclasses with comparable properties inside. This question targets the following quote from java.lang.Comaparable javadoc:

It is strongly recommended, but not strictly required that (x.compareTo(y)==0) == (x.equals(y)). Generally speaking, any class that implements the Comparable interface and violates this condition should clearly indicate this fact. The recommended language is “Note: this class has a natural ordering that is inconsistent with equals.”

Interesting fact for me here, is that official JavaDoc states that this convention is something that is highly recommended to follow, but if not (not expected behaviour) the respectful developer should warn users of API about this inconvenience.

Example

Let’s assume that we have class like:

data class Character(val name: String, var salary: BigDecimal)

Then generated code for equals() method will looks like:

public boolean equals(Object var1) {
  if(this != var1) {
     if(var1 instanceof Character) {
        Character var2 = (Character)var1;
        if(Intrinsics.areEqual(this.name, var2.name) && Intrinsics.areEqual(this.salary, var2.salary)) {
           return true;
        }
     }
     return false;
  } else {
     return true;
  }

}

In fact, from a practical perspective, for Comparable fields we usually have to invoke compareTo(....) == 0:

this.salary.compareTo(var2.salary) == 0

One of the most widespread example is fields of BigDecimal class where javadoc for equals() method states that:

Unlike compareTo, this method considers two BigDecimal objects equal only if they are equal in value and scale (thus 2.0 is not equal to 2.00 when compared by this method).

Of course, we can change this behaviour, by manually overriding equals() method like this:

override fun equals(other: Any?): Boolean {
    if (this === other) return true
    if (other?.javaClass != javaClass) return false
    other as Character
    if (name != other.name) return false
    if (salary.compareTo(other.salary) != 0) return false
    return true
}

But in case of data classes IDEA won’t help us with code generation for overridden equals()/hashCode() method (generation will be suggested only for toString() method. Very sad :frowning:
Proof:

Can someone from Kotlin core developers clarify the official position about equals() code generation?

P.S. When it comes to Java/Kotlin, for me it feels like missing option in method generation dialogue here:

Thank you in advance!


#2

Sorry, I’m not sure what behavior you expect here. equals() for a data class uses equals() for its components. I’m not sure how the fact that another interface exists and its javadoc says that it should be consistent with equals() is relevant.


#3

Hi, Dmitry!
It’s hard to say what i’m expecting here. I want to raise this question :slight_smile: and it would be nice to know what core Kotlin development team thinks about this code generation?

Because in Java ecosystem there are some crucial contracts/interfaces to follow and Comparable is definitely one of them. Another examples are Serializable, Externalizable, hashCode() and equals() contracts. For me, Comparable contract is not just another interface :slight_smile:


#4

I’m still not sure what answer you expect to hear. If you have a class that you want to use as a component of a data class, and that class implements compareTo() inconsistently with equals(), then that class is broken and you need to fix it. If that class implements compareTo() consistently with equals(), then it’s more efficient to compare it using equals(), so we do that.


#5

Another issue is that comparison is less strict than equality. In particular with types that only have a partial order (some elements are neither before or after, but are not the same element either - this can be represented as a directed acyclic graph and is actually quite common). These objects could implement Comparable but would have to use the 0 value to represent the undecided case (as comparison stability is important for users of Comparable). Being ordered in the same position does not require that the items are actually equal, and for partial ordering cannot do so.

If you’re wondering, Comparable is overspecified in that sense and using a less(Than) operator (as C++ does) may be better as for two elements a and b it allows both a < b = false and b<a = false. The downside is that you may not use the equals as if it would be the equivalent to testing both options (it is not in C++).


#6

Thank you for clarification!


#7

IMHO Comparable is exactly equivalent to C++ operator<. You case with both a<b and b<a being false maps directly to a.compareTo(b) being zero. Neither implies equality for a partial order.

Having compareTo consistent with equals is impossible in case of a partial order, as we’re missing the fourth outcome: incomparable. This obviously can’t fit into the signum of an int and is not common enough to get its own interface.


#8

Unfortunately Comparable is not equivalent to operator<. Look at the following fragment from the documentation of Comparator:

For the mathematically inclined, the relation that defines the imposed ordering that a given comparator c imposes on a given set of objects S is:

{(x, y) such that c.compare(x, y) <= 0}

The quotient for this total order is:

{(x, y) such that c.compare(x, y) == 0}.

It follows immediately from the contract for compare that the quotient is an equivalence relation on S, and that the imposed ordering is a total order on S

Operator< is, as discussed in this standard proposal that proposes some extensions to make different orderings explitic for most operators to only imply a “strict weak order”, but will even support strict partial order.

I know that ordering is rather technical computer science but can be important. At the very least, the Java Comparable/Comparator interface implies equality for results of 0, or at least sorting equality.


#9

@pdvrieze That’s an interesting link, but I can’t see there anything supporting your claim, that Comparable is not equivalent to operator<.

It rather recommends equality. Concerning “sorting equality”, I’m unsure what you mean (it could mean that they compare the same, but this is a tautology).


Given any antisymmetric and antireflexive relation < , I define

c.compare(x, y) = -1 iff x<y c.compare(x, y) = +1 iff y<x c.compare(x, y) = 0 otherwise

This would only blew if both x<y and y<x were true, which is a non-sense and violation of the above condition.

Setting
x<y iff c.compare(x, y) < 0
gives the inverse transformation. So IMHO the things are equivalent.


#10

The problem isn’t this, it is correct, it is even so that “naturally” c.compare(x,y)==0 if x.equals(y), but notice the lack of iff, it is not true in the reverse. Perhaps it is easiest to use an example. Let’s take a simple data class:

data class(val x:Int, val y:Int) {
  fun compareTo(other):Int = when {
    x < other.x && y<other.y -> -1
    x > other.x && y>other.y -> 1
    else -> 0
  }
}

Note that in the case of a(2,8) and b(3,7) that x<other.x and y>=other.y one cannot say a<b or b<a. These values are obviously not equal (nor will the default equals implementation treat them as that), but neither is smaller than the other. The compareTo function in this case only defines a partial order. You can also observe that it is properly reflexive (but would benefit from a stable sorting algorithm ;-)).


#11

Sure! We would also expect that exactly one of x<y, y<x and x==y is true, right?

And you could have written instead that both Point(2, 8) < Point(3, 7) and Point(3, 7) < Point(2, 8) are false. That’s well-known for point-wise comparison and that’s a problem. My point is that’s exactly the same problem for a Comparator as for a operator<.


#12

We don’t expect that exactly one of the values is true. In “normal” logic the expectation is that at most one is true. The point is, a key characteristic of a partial order is that there is a 4th class of positions where items are neither before or after but not equal either. The point is that partial ordering is quite common (a directed acyclic graph by its definition defines a partial order) and a programming language cannot ignore it. Even if Kotlin didn’t have to follow Java semantics, it is a dangerous game to equate equality with the same rank in ordering. In almost all programming cases (where compound types are much more common that in regular math).