Search code examples
kotlingenericscovariancecontravariance

Generic variance type parameter(Kotlin)


I do not fully understand how variance in Generics work. In the code below the classes are as follows Any -> Mammals -> Cats. Any is the supertype, there is a parameter called from in the copy function

From what I understand about the out and in keywords, out allows reference to any of it's subtype, can only be produced not consumed.

in allows reference to any of it's supertype, can only be consumed not produced.

However in the copytest function we are instantiating the function copy. I gave it a catlist1 argument in the from parameter. Since the parameter has an out keyword wouldn't it mean that we can only input parameters that are a subtype of catlist2?

To top my confusion off, I have seen many conflicting definitions, for instance, in Kotlin, we can use the out keyword on the generic type which means we can assign this reference to any of its supertypes.

Now I am really confused, could anybody guide me on how all of these work? Preferably from scratch, thanks!

class List2<ITEM> {
    val data = mutableListOf<ITEM>()

    fun get(n: Int): ITEM = data[n]

    fun add(item: ITEM) { data.add(item) }
}

fun <T> copy(from: List2<out T>, to: List2<T>) {

}

fun copytest() {
    val catList1 = List2<Cat>()
    val catList2 = List2<Cat>()
    val mammalList = List2<Mammal>()

    copy(catList1, mammalList)
}

Solution

  • I think maybe you're mixing up class-declaration-site generics and use-site generics.


    Class-declaration-site generics

    Defined at the class declaration site with covariant out, it is true you cannot use the generic type as the type of a function parameter for any functions in the class.

    class MyList<out T>(
        private val items: Array<T>
    ) {
        fun pullRandomItem(): T { // allowed
            return items.random()
        }
    
        fun addItem(item: T) { // Not allowed by compiler!
            // ...
        }
    }
    
    // Reason:
    
    val cowList = MyList<Cow>(arrayOf(Cow()))
    
    // The declaration site out covariance allows us to up-cast to a more general type.
    // It makes logical sense, any cow you pull out of the original list qualifies as an animal.
    val animalList: MyList<Animal> = cowList 
    
    // If it let us put an item in, though:
    animalList.addItem(Horse()) 
    
    // Now there's a horse in the cow list. That doesn't make logical sense
    cowList.pullRandomItem() // Might return a Horse, impossible!
    

    It is not logical to say, "I'm going to put a horse in a list that may have the requirement that all items retrieved from it must be cows."


    Use-site generics

    This has nothing to do with the class level restriction. It's only describing what kind of input the function gets. It is perfectly logical to say, "my function does something with a container that I'm going to pull something out of".

    // Given a class with no declaration-site covariance of contravariance:
    class Bag<T: Any>(var contents: T?)
    
    // This function will take any bag of food as a parameter. Inside the function, it will 
    // only get things out of the bag. It won't put things in it. This makes it possible
    // to pass a Bag of Chips or a Bag of Pretzels
    fun eatBagContents(bagOfAnything: Bag<out Food>) {
        eat(bagOfAnything.contents) // we know the contents are food so this is OK
    
        bagOfAnything.contents = myChips // Not allowed! we don't know what kind of stuff 
           // this bag is permitted to contain
    }
    
    // If we didn't define the function with "out"
    fun eatBagContentsAndPutInSomething(bagOfAnything: Bag<Food>) {
        eat(bagOfAnything.contents) // this is fine, we know it's food
    
        bagOfAnything.contents = myChips // this is fine, the bag can hold any kind of Food
    }
    
    // but now you cannot do this
    val myBagOfPretzels: Bag<Pretzels> = Bag(somePretzels)
    eatBagContentsAndPutInSomething(myBagOfPretzels) // Not allowed! This function would
        // try to put chips in this pretzels-only bag.
    

    Combining both

    What could be confusing to you is if you saw an example that combines both of the above. You can have a class where T is a declaration site type, but the class has functions where there are input parameters where T is part of the definition of what parameters the function can take. For example:

    abstract class ComplicatedCopier<T> {
    
        abstract fun createCopy(item: T): T
    
        fun createCopiesFromBagToAnother(copyFrom: Bag<out T>, copyTo: Bag<in T>) {
            val originalItem = copyFrom.contents
            val copiedItem = createCopy(originalItem)
            copyTo.contents = copiedItem
        }
    }
    

    This logically makes sense since the class generic type has no variance restriction at the declaration site. This function has one bag that it's allowed to take items out of, and one bag that it's allowed to put items into. These in and out keywords make it more permissive of what types of bags you can pass to it, but it limits what you're allowed to do with each of those bags inside the function.