Overloading == with different types of operands


#1

Newbie here. Hopefully I’m missing something.

I’m building a library for exact fractional decimal math using operator overloading. The arithmetic operators work. The fact that assignment overloading is not supported is inconvenient for the application programmer using the library, but it’s not a showstopper. However, I’ve just now bumped into what I really hope is my mistake. Here’s the general outline of the code:

class Num : Comparable<Any> {
 ...
    override fun equals(other: Any?): Boolean { ... }
    override fun compareTo(other: Any): Int { ... }
}

All comparisons between Num and Num work, and the < <= > >= comparisons between Num and Int, Long and Double also work, but the second of these lines of code fails to compile:

val x = Num(10)  // means initialize a Num variable with the integer value ten
    if (x == 10) ...

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

Am I missing something? I’m hoping this is just a bug - that’s why I posted it in Support instead of Language Design. If it is disallowed by the Kotlin language design, will that be changed? If so, when?

TIA


Kotlin or Swift? (isn't a flame war, its about opinion)
New operators
#2

Look here for documentation. Operator overloading requires additional keyword operator.


#3

The Kotlin in Action books says…

there are no specific functions for overloading the six comparative operators. Instead the compiler just uses equals( ) and compareTo( ), and that those methods are not marked with the operator keyword.

That said, the problem isn’t in the implementation of Num, the problem is that the compiler flags it as an error.

Note that

  if (x >= 10 && x <= 10) ...

compiles and works fine!


#4

What you’re trying to do, doesn’t work in any case. The equals() contract requires that if a.equals(b) then also b.equals(a) (see Effective Java for example). You can’t change the Int class to make it’s equals return true when you give your Num as argument.


#5

You are correct. The problem is that equals expects Any but gets Int. This example works:

class Num(val i:Int){
    override fun equals(other: Any?): Boolean {
        return i == other
    }
}

fun main(args: Array<String>) {
	assert(Num(5) == (5 as Any))
}

I am not sure why it works this way.


#6

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).


#7

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.


#8

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.


#9

It is doubly suspicious since comparison operations work as expected.


#10

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!


#11

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.


#12

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.


#13

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?


#14

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.


#15

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:


#16

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.


#17

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.


#18

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).


#19

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