Overloading == with different types of operands

Jonathan - Of interest, I understand the transitivity issue and dealt with it for + - * / by adding extensions to Int, Long and Double like this:

operator fun Double.plus(other: Num): Num { ... }

So that this code works fine:

    val n = Num(11)
    val d = 14.0

    print(n + d)
    print(d + n)

Programmers who will use Num won’t care about the implementation. They just want it to work. But not having the equality operators is a showstopper.

The designers of Kotlin went to a lot of trouble to be consistent, elegant and unsurprising. Those concepts seem to have been lost in the design of the implementation of operator overloading (and the fact that assignment isn’t even an operator).

You just can’t fix the transitivity issue for equals(). I guess what Kotlin tries to do in your case is comparing a primitive Java int with your object. It will never be equal. Just like 3 == 3.0 will not be true and will not even compile (unless you autobox the primitives by casting to Any). It’s a limitation you have to work with.

The transitivity issue aside, it does seem to me to be a bug or, at best, undocumented behaviour.

The reason I say that is because the following compiles and runs fine:

    val x = Num(10)
    if (x.equals(10)) ....

and the ‘==’ operator is supposed to be translated into something similar which allows for null screening.

I can’t find anything which says the expression on the RHS can’t be a sub-type of the parameter type.

It is doubly suspicious since comparison operations work as expected.

Limitations suck. Fortran 77 here we come!!

So I’ve added these infix functions:

infix fun Any.EQ(other: Any): Boolean { ... }
infix fun Any.NE(other: Any): Boolean { ... }
infix fun Any.LT(other: Any): Boolean { ... }
infix fun Any.LE(other: Any): Boolean { ... }
infix fun Any.GT(other: Any): Boolean { ... }
infix fun Any.GE(other: Any): Boolean { ... }

and they work great!

I looked through the standard library and tried to understand why does .equals work but == - not and have not found anything relevant. It seems that there is additional type check in compiler itself when it tries to convert operator to method. For now it does not make sense.

1 Like

I’ve also had a look at the standard library and, like @darksnake, couldn’t find anything directly relevant though I did find this description of the equals() method under the Any class.

Notice that it says that implementations must fulfil several requirements including ‘symmetricity’ if that’s the correct word. This is presumably because there are methods in the standard library which assume that the equals() method is symmetric and so, if it’s not, it could lead to serious problems in user code.

It looks to me like the compiler is trying to check these requirements when the ‘==’ operator is used and so, in the case of Int, it ‘knows’ that it can’t possibly be symmetric with regard to the ‘Num’ class because you can’t override Int’s inherited equals() method with an extension to achieve that. It therefore flags it as an error.

However, when the equals() method itself is used the compiler is faced with a quandary because, for all it knows, the code within the overridden method might be transforming the Int into a Num before doing the comparison. It therefore allows it.

Although it’s inconvenient from @DonWills point of view, that’s the only sense I can make of it. However, if this analysis is near the mark, I think it should be explicitly covered in the documentation to avoid further head scratching on these sort of points.

There’s another solution so that the == and != operator functionality will work in all cases. Instead of using equals( ), the compiler can generate the code to use compareTo( ) for all six relational operators. How do I request that change?

I don’t think you can :frowning:

Although when I’m overriding the equals() method for my ‘Comparable’ classes, I generally delegate to the compareTo() method with a line such as this:

   return this.compareTo(other) == 0

the problem is that the compiler still won’t let you use the ‘==’ or ‘!=’ operator for a class such as yours. You either have to use equals() directly or write your own infix functions such as the Fortran style: eq or ne.

I think myself that the latter is the best solution as you’ll then be able to preserve symmetry by writing extension functions for the primitive types being equal or not to your Num instances.

Incidentally, if you do go with the infix function solution, then you’d still be able to compare two Num instances for equality (or one instance for nullity) using the ‘==’ or ‘!=’ operators, just by overriding equals() in the usual fashion - let’s say:

 override fun equals(other: Any?): Boolean {
     if (other !is Num) return false
     return this.compareTo(other) == 0
 }

.....

val x = Num(10)
val y = Num(10)
if (x == y) println("They're equal") 
if (x != null) println("Not null")

You’d only then need to use the infix functions when comparing against primitives. Possibly a bit confusing but just an idea :slight_smile:

alanfo - Thanks for the suggestion, but the confusion about which combinations work and don’t work means it is not a viable option for the programming staff who will use the code.

The compiler error message is Operator ‘==’ cannot be applied to ‘Num’ and ‘Int’

… and, surprising as it may be, it does the Right Thing.

Equality in Kotlin is modeled very closely to object equality in Java, which is required for interoperability.
Values are compared using method equals, which is defined in kotlin.Any (and java.lang.Object).
This equality is used, for example, in standard collections, where values can be stored in sets or used as keys in maps.

Now, consider your class Num. It can’t be equal (in the terms above) to other number representations on JVM platform (java.lang.Integer, java.math.BigInteger, java.math.BigDecimal, to name a few), because these classes know nothing about Num. Even if you define equals for Num so that , for example, Num(1).equals(Integer(1)), it wouldn’t be symmetric: Integer(1).equals(Num(1)) will, of cause, return false. This will cause a whole lot of problems for everyone who relies on requirements for method equals, starting from standard collections.

That’s why, for example,

BigInteger.ONE.equals(Integer(1))
=> false

TL;DR: don’t do that, it will break a hole in time-space continuum.

2 Likes

But wait, it just hit me that assignment is not an expression in Kotlin, right? So there would be no conflict in using = also for comparison. When you write
(if x = 5)
instead of the compiler emitting “only expressions are allowed in this context”, it could translate = to compareTo()==0, or, to minimize risk of breaking existing code, to some new method comparesAsEqual() (if it exists on both objects, those who want to use it could implement it as extensions).

This way, we don’t need a new (ugly) operator, but could use the quite natural =, which would be the obvious choice had it not been appropriated for assignment. (We could still add <> for testing negative).

Sorry, last post was to go to New operators - #9 by nbengtg
It may be relevant here also, though, so I let it stand.

It does not do the right thing: Consider for example implementations of complex numbers or similar concepts like complex powers or vector spaces. It is perfectly reasonable to have classes for real, imaginary and complex numbers and these “unrelated” classes may very well be equal.

Also, if we should not “combine” “unrelated” classes, why can we then use operators like +, -, *, / but not ==?

Because ==/equals needs to be symmetric to work in collections. Otherwise you add Fraction(10, 1) to a list, and then list.contains(10) may or may not return true. You can’t extend the Int class to be equal to some random class you write.

If you want to have your own number classes, write them all yourself and give them all proper equals implementations, so they may be equal to themselves. For example you could make Fraction(10,1) to be equal to Complex(10, 0) and equal to MyInt(10). But you would need to create that logic yourself.

The Java/Kotlin number classes have no equals cross-compatibility. There is no easy build-in way to test, if for example a double is equal to a long, because the conversion in both directions may be lossy.

But this does not work, because I could never have Complex(10,0) == Fraction(10, 1), because of the wrong compiler error. I can write correct equals but I cannot use ==.

You should also note that + and * should also be symmetric (among other things).

What? Of course you can do that. The compiler error happens because you’re comparing primitive Ints and in that case the compiler knows that you’re doing things that are impossible to work.

That is what one would expect, but you would get a compiler error stating: Operator '==' cannot be applied to 'ComplexPower' and 'ActivePower', in my application. Using equals directly works.

Similarly, Kotlin tests AssertEquals fails but using JUnits AssertEqual works however, apparently because there Kotlins compiler does not do its wrong check.

This looks like a bug: https://youtrack.jetbrains.com/issue/KT-4071