Operator overloading on += for set and get calls wrong setter

Viewed 82

I have made an extension functions for BigIntegers, allowing me to add Ints to them.

operator fun BigInteger.plus(other: Int): BigInteger = this + other.toBigInteger()
// Allowing me to do
val c = myBigInt + 3

I have also made a Counter class, holding bigintegers for various keys, for easy counting. Since doing counter["1"] += myBigInt isn't allowed on standard maps (it's nullable), I have added a custom getter that returns a default value, making this possible.

class Counter<K>(val map: MutableMap<K, BigInteger>) : MutableMap<K, BigInteger> by map {
    constructor() : this(mutableMapOf())

    override operator fun get(key: K): BigInteger {
        return map.getOrDefault(key, BigInteger.ZERO)
    }

I can then use it like this

val counter = Counter<String>()
c["ones"] += 5.toBigInteger()

Problem is that I cannot use it like this:

c["ones"] += 5 // doesn't work, "Kotlin: No set method providing array access"

but this should be equivalent to this, which works, since it should use my extension operator on the bigint:

c["ones"] = c["ones"] + 5 // works

Why doesn't this work?

I've tried adding a set method for Ints, but then I see a very weird behavior. Kotlin will do the calculation correct, but then convert the BigInteger to an Int before passing it to my class! Example:

inline operator fun BigInteger.plus(other: Int): BigInteger {
    val bigInteger = this + other.toBigInteger()
    println("calculated bigint to $bigInteger")
    return bigInteger
}
class Counter<K>(val map: MutableMap<K, BigInteger>) : MutableMap<K, BigInteger> by map {
    constructor() : this(mutableMapOf())

    override operator fun get(key: K): BigInteger {
        return map.getOrDefault(key, BigInteger.ZERO)
    }

    operator fun set(key: K, value: Int) {
        println("setting int $value")
        map[key] = value.toBigInteger()
    }
}
val c = Counter<String>()
c["1"] = "2192039569601".toBigInteger()
c["1"] += 5
println("result: ${c["1"]}")
c["1"] = "2192039569601".toBigInteger()
c["1"] = c["1"] + 5
println("result: ${c["1"]}")

Which prints

calculated bigint to 2192039569606
setting int 1606248646 <--- why does it call the int setter here?
result: 1606248646

calculated bigint to 2192039569606
result: 2192039569606

Why does Kotlin do the BigInt summation, but converts it back to an Int before sending to my setter?

Update

Since a comment suggest this is a compiler issue, any other ideas? My ultimate goal here, was to have a counter of big integers, but to be able to easily add ints to it.

Adding this as a set function, makes it being called for both ints and bigints, so I can do the proper assignment myself. However, it will also then allow someone to add floats that will crash at runtime.

operator fun set(key: K, value: Number) {
    map[key] = when (value) {
        is BigInteger -> value
        is Int -> value.toBigInteger()
        else -> throw RuntimeException("only ints")
    }
}

Any tips?

1 Answers

Notice that c["ones"] += 5 can be translated into calls in two ways:

c.set("ones", c.get("ones").plus(5))

c.get("ones").plusAssign(5)

The first way is what your code currently translates to, because you don't have a plusAssign operator defined. As I said in the comments, there is a bug in the compiler that prevents the operators from resolved correctly. When resolving c["ones"] += 5, It seems to be trying to find a set operator that takes an Int instead (possibly because 5 is an Int), which is unexpected. If you modify the code in the bug report a little, you can even make it throw an exception when executed!

class Foo {
    operator fun get(i: Int) : A = A()
    operator fun set(i: Int, a: A) {}
    operator fun set(i: Int, a: Int) {}
}
class A {
    operator fun plus(b: Int) = A()
}

class B
fun main(args: Array<String>) {
    val foo = Foo()
    foo[0] = foo[0] + 1
    foo[0] += 1 // this compiles now, since there is a set(Int, Int) method
                // but A can't be casted to Int, so ClassCastException!
}

It is rather coincidental (and lucky) in your case, that the compiler knows how to convert from BigInteger (or any other Number type actually) to Int, using Number#intValue. Otherwise the program would have crashed too.


A natural alternative way is to define the plusAssign operator, so that the assignment gets translated the second way. However, we can't do it on BigInteger, because plusAssign would need to mutate this, but BigInteger is immutable. This means that we need to create our own mutable wrapper. This does mean that you lose the nice immutability, but this is all I can think of.

fun main() {
    val c = Counter<String>()
    c.set("1", "2192039569601".toMutableBigInteger())
    c.get("1").plusAssign(5)
    println("result: ${c["1"]}")
}

data class MutableBigInteger(var bigInt: BigInteger) {
    operator fun plusAssign(other: Int) {
        bigInt += other.toBigInteger()
    }
}

fun String.toMutableBigInteger() = MutableBigInteger(toBigInteger())

class Counter<K>(val map: MutableMap<K, MutableBigInteger>) : MutableMap<K, MutableBigInteger> by map{
    constructor() : this(mutableMapOf())

    override operator fun get(key: K): MutableBigInteger {
        return map.getOrPut(key) { MutableBigInteger(BigInteger.ZERO) }
    }

    operator fun set(key: K, value: Int) {
        println("setting int $value")
        map[key] = MutableBigInteger(value.toBigInteger())
    }

}

Notably, getOrDefault is changed to getOrPut - when a value is not found, we want to put the zero we return into the map, rather than just returning a zero that is not in the map. Our changes to that instance wouldn't be visible through the map otherwise.

Related