kotlingenericstype-inference

Strict generics in Kotlin


Given I have the following code

interface Animal {}

class Cat() : Animal {}

class Dog() : Animal {}

class MyClass<B, A : List<B>>(val list1: A, val list2: A) {}

It looks like I can call the constructor with different list types like this:

val myClass = MyClass(listOf(Cat()), listOf(Dog()))

Is there a way to instruct the Kotlin compiler that both lists should have the same strict kind?

The goal is that I can call:

val myClass = MyClass(listOf(Cat()), listOf(Cat()))
val myClass = MyClass(listOf(Dog()), listOf(Dog()))

But I can't call:

val myClass = MyClass(listOf(Cat()), listOf(Dog()))
val myClass = MyClass(listOf(Dog()), listOf(Cat()))

Before you ask, the reason I need this is so I can implement two methods on MyClass like this:

class MyClass<B, A : List<B>>(val list1: A, val list2: A) {
    fun moreSameAnimals(list: A) = print("same")
    fun <C : List<B> moreDifferentAnimals(list: C) = print("different")
}

And have the compiler chose the appropriate method depending on the single type of Animal present in MyClass


Solution

  • Disclaimer: question seems a little strange to me. Usually we don't target specific types of lists, but in this case it looks like we need to differentiate not between types of animals, but between types of lists. Author of the question said this is as it should be, so I answer the question directly.

    Problem is caused by the fact that the List<Animal> is a supertype of List<Dog> and in most cases the compiler is allowed to make upcasts implicitly. Please note even if we disallow different types of animals in MyClass, the user can still do this:

    MyClass(listOf(Cat()) as List<Animal>, listOf(Dog()) as List<Animal>)
    

    Or this:

    foo(listOf(Cat()), listOf(Dog()))
    
    fun foo(list1: List<Animal>, list2: List<Animal>) = MyClass(list1, list2)
    

    This is actually what happens if we call the MyClass directly - it simply upcasts both lists to List<Animal> and then A is exactly the same in both arguments.

    There is a hidden feature in Kotlin which enables exactly the behavior you described. We need to annotate the type parameter with @OnlyInputTypes. Problem is: this annotation is internal and we can't use it easily. Still, with some hacks and tricks we can, if this suits you.

    Another problem is that I believe this annotation could be used with functions only. We can workaround this problem by using a factory function instead of a constructor:

    fun main() {
        val myClass1 = MyClass(listOf(Cat()), listOf(Cat())) // compiles
        val myClass2 = MyClass(listOf(Dog()), listOf(Dog())) // compiles
        val myClass3 = MyClass(listOf(Cat()), listOf(Dog())) // fails
    }
    
    class MyClass<A> private constructor(val list1: List<A>, val list2: List<A>) {
        companion object {
            @Suppress("INVISIBLE_MEMBER", "INVISIBLE_REFERENCE")
            operator fun <@kotlin.internal.OnlyInputTypes A> invoke(list1: List<A>, list2: List<A>) = MyClass(list1, list2)
        }
    }
    

    Please note this technique doesn't prevent user to upcast manually or do the same as in the above foo example.