Covariance and Contravariance issue

class MyLinkedList<TYPE> {
    class Node<TYPE>(
        val value: TYPE,
        var next: Node<TYPE>? = null
    )
    private var head: Node<TYPE>? = null
    var size: Int = 0
        private set
    fun insert(value: TYPE) {
        head = Node(value, head)
        size++
    }
}
fun <ITEM_TYPE> copy4(from: MyLinkedList<in ITEM_TYPE>, to: MyLinkedList<outITEM_TYPE>) {
    (0 until from.size).forEach {
        to.insert(from[it])//under from[it] it says:
 /**Type mismatch.
Required:
Nothing
Found:
Any?**/
    }
}

I am swapping ā€œinā€ and ā€œoutā€ intentionally, to understand the error.

I know the issue resides on ā€œinā€ and ā€œoutā€, but just want to know.

I understand it like this : it is related to the fact that ā€œoutā€ is just for returning and we are using it a position of an argument that is from[it] ā€œitā€ here an item from ā€œfromā€ argument that is defined as out, but it is in a position of argument."
1- is my understanding corrct?
2- And why compiler interpreted them into ā€œNothingā€ and ā€œAny?ā€?
3- does the compiler catch the type of ā€œfromā€ and baed on that, it treat the logic of ā€œinā€ and ā€œoutā€?
My instructor explains: ā€œwhen we float over this, itā€™s saying that ā€œIā€™m requiring nothingā€ thatā€™s because the ā€œtoā€ side that says ā€œIā€™m taking something as an output parameterā€, ā€œI canā€™t call anything that actually takes an argument coming inā€ so it actually replaces the type of those arguments with nothing.ā€

  1. Yes, your understanding is correct.
  2. in/out basically means that we donā€™t know what types are produced/consumed respectively. So this is similar to a star projection, If we produce and we donā€™t know what is the type, we still can safely assume it is at least Any?, because all objects are Any?. If we consume, but we donā€™t know what type to use, we canā€™t call the function safely. In this case Kotlin uses Nothing to represent an ā€œimpossibleā€ type, so we canā€™t pass anything there.
  3. I didnā€™t get this question.
1 Like

The way I think of it is:

  • out (covariant) means roughly ā€˜ā€¦or a subtypeā€™.
  • in (contravariant) means roughly ā€˜ā€¦or a supertypeā€™.

(Those relationships are clearer, though more long-winded, in Java, where covariance is indicated by ? extends <type>, and contravariance by ? super <type>.)

In this case, inside copy4() all we know is that to is a MyLinkedList of some subtype of ITEM_TYPE. But thatā€™s all we know ā€” we canā€™t narrow it down to any particular subtype. At the most extreme, it could be Nothing (which is a subtype of all types).

Similarly, from is a MyLinkedList of some supertype of ITEM_TYPE ā€” but again, the compiler canā€™t say anything about which supertype, except that itā€™s Any? or some subtype.

So the only type we know we can insert() into to is Nothing ā€” but what weā€™re trying to insert could be Any?. Hence the error message.

1 Like

Thank you very much, I got it

Thank you very much.
3d question is when the compiler catches the from type. Let say the type here is Animal. Based on this it enforces the second argument to be its subtype that is Cat or Dog?

I think this is probably one of the very few things about Kotlin that I actually donā€™t like and I think Java does better. I have NEVER managed to wrap my head around in and out and where youā€™re supposed to use one and not the other. I understand the basic concept of in is for consuming and out is for producing, but when I try to actually use it in code, I always get it wrong. I think Javaā€™s syntax is easier to understand.

3 Likes

for me in/out or super/extends are a different view of the problem,
one describes the usage in=consumer out=producer (which are reasonable names, right?), the other is a language theoretical thing (which explains which types you can use).

Maybe the problem with something like copy(from, to), is that you think of the parameters as from=read=in / to=write=out. But thatā€™s a different thing.
Looking at the type it seems to be clear, from uses a type that produces values (out) and to uses a type that consumes values (in).

So, from is something you read, but you are reading from the other end which is a producer.

I personally like in / out because itā€™s a simpler model (not type theory), but I also have to remember, that itā€™s the other end of what I usually think first.

On the other side, with super / extends I have to think about the consequences the data direction has for the types, which is not directly obvious to me.
Fortunately, I never had to learn modern Java.

2 Likes

One way of thinking about this is that out will make all <T> arguments to methods into Nothing. This is because you said the type can only be output, not taken in ; that means nothing is known about the type that can be taken in (there can be arbitrary limits on it) so you can only disallow any values to be passed, because any value may not match the constraints. So the compiler uses the type that admits no value, which is called Nothing.

Conversely, in will make all <T> return values into Any?, because you marked the type as being only inputtable. You do know the output methods output something, but since you know nothing about the value you use the type that can be anything, which is called Any?.

1 Like