1

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

3
  • 1
    Why do you make the whole list type generic? Do you plan your class to target specific implementations of lists (e.g.: MyClass<LinkedList<String>>? If not, it is probably better to skip A.
    – broot
    Commented Sep 5 at 20:24
  • 1
    Also, I'm not entirely sure what is your expected behavior and the difference between moreSameAnimals and moreDifferentAnimals. Again, it feels you differentiate on the type of the list, not type of animals. Feels a little like an XY problem. You explained what kind of code you would like to write, but you didn't really explain what kind of behavior you need. I suppose you already made some incorrect assumptions about generics, so the question is a little misleading.
    – broot
    Commented Sep 5 at 20:34
  • That's because you're looking at a very simple example for something that I need in a much more complex scenario - the question is valid Commented Sep 5 at 20:54

2 Answers 2

2

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.

1
  • Question is a little strange, but you nailed it, thank you. In the end I sorted it out in a different way by dropping a requirement, but I certainly learned something today. Commented Sep 6 at 18:55
2

The problem you describe is due to Kotlin's type-inference.

You can see this working in a simpler example using the built-in listOf() function. The compiler infers the generic type based on the type of the values provided to the function.

val a = listOf(3) // inferred type is List<Int>
val b = listOf("text") // inferred type is List<String>

However, if you provide arguments with different types, the compiler needs to find a common supertype among the arguments. That unified supertype of all non-nullable types is Any. Add nullable types into the mix and the unified supertype is Any?.

val c = listOf(3, "text") // inferred type becomes List<Any>
val d = listOf(3, "text", null) // inferred type becomes List<Any?>

In your case the generic type when passing arguments of types Dog and Cat is inferred to the common supertype Animal.

You can avoid this behaviour by declaring the desired type explicitly.

// this works
val myClass = MyClass<Cat>(listOf(Cat()), listOf(Cat()))
val myClass = MyClass<Dog>(listOf(Dog()), listOf(Dog()))
// this does not compile
val myClass = MyClass<Cat>(listOf(Cat()), listOf(Dog()))
val myClass = MyClass<Dog>(listOf(Dog()), listOf(Cat()))

// the above assumes a simplified version of MyClass
class MyClass<A>(val list1: List<A>, val list2: List<A>) {}
1
  • Yeah, I am aware of type-inference, my question is if there is a way to avoid it and only accept exact same types instead of inferring common super type Commented Sep 5 at 21:13

Not the answer you're looking for? Browse other questions tagged or ask your own question.