kotlinassignment-operator

hard-to-understand error in an augmented assignment statement in Kotlin


This is a contrived example for the sake of my understanding Kotlin language. The class Box<T> below models a box that contains boxes of the same type T.

The question is why arr += x causes compile error while arr.add(x) does not. The error message is:

Type mismatch. Required: kotlin.collections.ArrayList<Box>

Found: List<Box>

the property arr is immutable and points to a mutable list. So I believe there is no ambiguity whether arr += x should mean arr = arr + x or arr.add(x). It should mean arr.add(x)

I also noticed that the issue is gone if generic type Box implements Iterable<T> instead of Iterable<Box<T>>

So, what is the reason of the error?


class Box<T>(private val arr: ArrayList<Box<T>> = ArrayList()) : Iterable<Box<T>> {
    private val id: Int = seq++

    override operator fun iterator(): Iterator<Box<T>> {
        return object : Iterator<Box<T>> { 
            private var i = 0;
            override fun hasNext(): Boolean {
                return i < arr.size
            }

            override fun next(): Box<T> {
                return arr[i++]
            }
        }
    }

    fun add(x: Box<T>) {
        // arr.add(x) // OK 
        arr += x // Error: Type mismatch.
    }

    companion object {
        var seq: Int = 0
    }

    override fun toString(): String {
        return "Box[$id]"
    }
}

fun main() {
    val box = Box<Int>(arrayListOf(Box(), Box()))
    for (x in box) {
        println(x)
    }
}

Solution

  • arr += x is neither arr = arr + x (though it could be in other cases) nor arr.add(x) (not directly, at least). It is just a call to the plusAssign operator function.

    There are many overloads of plusAssign. The relevant ones to this question are:

    operator fun <T> MutableCollection<in T>.plusAssign(
        element: T)
    
    operator fun <T> MutableCollection<in T>.plusAssign(
        elements: Iterable<T>)
    

    The first overload would call arr.add(x), and the second would call arr.addAll(x).

    The problem is, both of these are applicable in your case, but there is no "most specifc" overload among them.

    The first overload is trivially applicable, and the second overload is applicable because x is also a Iterable<Box<T>> because of the interface implementation.

    None of these are more specific because none of them can "forward" the call to the other. An overload is the most specific if it is the single overload that can "forward" to all the other applicable overloads. In this case:

    fun <T> MutableCollection<T>.plusAssign1(x: T) {
        this.plusAssign2(x) // does not compile
    }
    fun <T> MutableCollection<T>.plusAssign2(x: Iterable<T>) {
        this.plusAssign1(x) // also does not compile
    }
    

    See also the Kotlin Language Specification.

    I would just call add manually to fix the error, instead of using operators.