Should this compile(generics)?

Am I not understanding how generics works in Kotlin? Here is the minus operator for iterables in the standard kotlin lib:

public operator fun <T> Iterable<T>.minus(elements: Iterable<T>): List<T> {
    ...
}

So the generic type of this mist to be equal to the other iterable, right? If so, then how come this compiles?

val list1: List<String> = listOf()
val list2: List<Int> = listOf()
val list3 = list1.minus(list2)

The type of list3 is List<{Comparable*> & java.io.Serializable}>.

If this is intended then I have to say it’s very easy for mistakes to go undetected.

The nature of generics forces this.

Both Int and String are Comparable so here the compiler comes to the conclusion that you want to perform your operation on Comparable instances.

The mistakes will be caught when you try to use list3 and you use something that is not provided by Comparable.

val neg = list3.map { - it }

Now, the compiler will mark the - as an error because there is no minus operator defined for comparable and serializable.

I use generics quite a lot and they are pretty hard. Truth to be told, it is like the fifth time I read the generics documentation and I still don’t feel fully comfortable about it. On the other hand, I feel generics in Kotlin are pretty well designed, in my experience the compiler catches all mistakes I make.

Fine, they have a common interface. I can accept that. But here is an other example:

data class Foo(val foo: String)
data class Bar(val bar: String)

fun main() {
    val list1 = listOf<Foo>()
    val list2 = listOf<Bar>()
    val list3 = list1 - list2 //Type is List<Any>
}

Can you explain this?

In Kotlin Any is the supertype of all classes, therefore it is the common interface in this case.

I understand why that thing is confusing. Truth to be told, you have a point about it, only I don’t see how to make it better.

Actually, while I understand that for example list1 + list2 should return List<Any>, I think it doesn’t make sense for list1 - list2, because in this case the resulting list can only contain elements from list1. If the operator would not be declared as:

operator fun <T> Iterable<T>.minus(elements: Iterable<T>): List<T>

, but elements would be: Iterable<*>, then it would work as expected.

Maybe I don’t see a full picture here.

One relevant factor that I don’t think anyone’s mentioned yet: kotlin.List is covariant, i.e. its type parameter is declared as out E.

That means that because both Int and String are subtypes of Comparable, so List<Int> and List<String> are subtypes of List<Comparable>. (This is not the case for MutableList, which is invariant.)

That’s how the compiler can infer a List which is a supertype of the given ones.

1 Like