Search code examples
kotlingenericscollections

Kotlin - Lists operations between lists with different T's


I stumbled upon a bug in my code that I thought would be noticed & prevented by the compilation:

val ints: List<Int> = listOf(1, 2, 3)
val uuids: MutableList<UUID> = mutableListOf(UUID.randomUUID())

// No compilation error here:
val result = uuids.intersect(ints)

// Compilation error here, as expected
uuids.add(1)

IJ mentions that result is of type Set<{Comparable<*> & Serializable}>

And intersect signature seems correct: public infix fun <T> Iterable<T>.intersect(other: Iterable<T>): Set<T> {

My bug was that I just checked wether the intersection was empty, thus not seeing that by comparing incompatible objects, that would always be true.

So do I miss something obvious? Thanks


Solution

  • I agree that it's a bit surprising that it is valid to call intersect with List objects of two different types. However, this is an inevitable consequence of Kotlin's type inference system and the type projection on List objects.

    Type inference

    No type is specified in your call uuids.intersect(ints) so Kotlin checks to see if a type exists that makes the call valid, and for the most specific type that the output can be. The answer to that there is such a type so the call is valid; and that type is a Set of Comparable<*> & Serializable objects. Thus Kotlin makes that type the generic type T for the call.

    This is the case because both List<UUID> and List<Int> are both subtypes of List<Comparable<*> & Serializable> (not that you can write that intersection type in Kotlin1). In turn, this is true because both UUID and Int are subtypes of both Comparable<*> and Serializable, and Lists are covariant2.

    I don't see any way the signature of the function intersect could be written to avoid this problem, barring some special Kotlin annotation to change the usual type inference algorithm.

    Invariant types

    Instead, if you have a function of the form:

    fun <T> GenericType<T>.doSomething(target: GenericType<T>)
    

    and GenericType was invariant (ie no type projection) then you will get an error if you try to call it with an argument GenericType<S> (where S is not the same as T). I suspect this case you would more often come across, and this would not give surprising results.

    Specifying a type

    If you specify the type for your call, either as the type of result or the generic type of the call as UUID (the type you were surely expecting), then you do get an error:

    val result: Set<UUID> = uuids.intersect(ints) // type mismatch error
    val result = uuids.intersect<UUID>(ints) // type mismatch error
    

    In such cases, there is no type inference to do and the compiler can immediately see there is a mismatch.


    1 See the YouTrack issue for the feature request

    2 ie out projection types, meaning a List<A> is a subtype of List<B> if A is a subtype of B