Generic variance: can't use var with <out T> and can't use val with <in T>?

So, I vaguely understand that a generic class declared with an “out” is a “producer” and if declared with an “in”, it is a “consumer”.

All in all, what I understand from this is that functions declared within the generic class can only either produce a result of type T (out), or take in as an argument a type T (in), but not both.

I also suppose this means that an “out” generic class is read-only, while an “in” generic class is write-only.

Why though are there restrictions on a property of type T?
Upon declaring a generic class of type <out T>, any vars of type T are marked as inappropriate. This, I can kind of understand, as “out” generics are “read-only”, and therefore wouldn’t support a var which could allow re-assigning or re-writing to the var variable.

But why can’t I use a val in a generic of <in T>? I don’t feel like I am producing anything when I have a val of type T.

A generic class that has an type parameter can be down casted to any lower class. This is because it is a consumer, as in it only takes in values of type T. The reason why you can’t do that is because say for example that you have a class Container<in T> with a val property of type T.
Because this container is a consumer (i.e it’s type parameter is declared with an in), then you can for example safely cast a Container<Any> into a Container<String> because Container is supposed to be able to consume any object of type Any, which means that because String inherits from Any (i.e. String’s superclass is Any), that same Container should be able to accept values of String, too. But now if you can safely cast a Container<Any> to a Container<String>, and you have a val property that you try to access, that val property was beforehand allowed to be of type Any, but now you are trying to cast it to a String, so boom ClassCastException.

Here’s a minimal example that shows that with those same classes:

class Container<in T>(val content: T) //Compiler error right here

fun main() {
    val cont = Container<Any>(Any()) // Just creating a new Any object for this example
    val contString: Container<String> = cont //This is totally legal because in guarantees downcasting safely
    val str: String = contString.content //Whoops, you are now trying to cast an Any to a String.
}

Rule of thumb is that in consumers guarantee safe downcasting (i.e. from an Any to a String for example), while out producers guarantee safe upcasting (i.e. from a String to an Any because if some other part of your program expects a producer to give them an Any and it gives them a String then that’s totally fine, but the opposite isn’t).

1 Like

Thanks again for the reply @kyay10.
I’m not quite sure I’m following though. From what I understand, <in T> sets up a contravariant situation, allowing you to safely cast a Container<String> into a Container<Any>. I feel like you might be describing a covariant situation, but honestly, I’m not good at this. That being said, the code you provided does indeed seem to be casting a Container<String> into a Container<Any>.

Also, I think there are some portions of your text not showing up. I’ve noticed that when using angle brackets ( ‘<’ and ‘>’), as in typing Container<String>, you have to use the the backslash escape character '\ 'before the ‘>’ or it doesn’t show up, like so: Container<String\>. For example, the following is supposed to say “Container<String> = Container<Any>” but I have left out the escape character:
Container = Container.
This goes away when marking it as code text of course. Anyway, areas that you might have typed <String> or <Any> are missing in your text so I have a hard time following.

1 Like

whoops, I’m sorry. It looks fine on my end for some reason. Regardless, cont is a Container of Any, contString is a Container of String, which gets assigned to cont (because in means that we can safely downcast, i.e. take a Container of Any and cast it to a Container of Int or String or whatever our heart desires because in allows the class to only consume those values, and so if a class can consume parameters of type Any, it can also consume parameters of type String or Int. The opposite is not true tho, because casting a Container of String to a Container of Any is problematic cuz it imples that the functions on Container that accept a String now suddenly can also accept an Any. out, on the other hand, means that we only produce/expose values to the outside world, so it is fine if we are exposing Strings for example and someone upcasts that class as producing Anys, because all Strings are Anys)

My bad, your code is fine. I can read that perfectly. It’s in the text above the code where stuff went missing, for example, when you typed “But now if you can safely cast a Container to a Container”, I’m assuming it was supposed to say Container<String> to a Container<Any> or something similar.

1 Like

contravariant situations allow you to safely downcast a Container of Any to a Container of String, basically limiting what that container accepts as input values, which is fine because every String is an Any

Oh, that also looks wrong on my screen, my bad. I’ll fix it rn!

Also, the Kotlin documentation describe the whole covariant vs contravariant thing probably more elegantly than I ever could:

The general rule is: when a type parameter T of a class C is declared out , it may occur only in out -position in the members of C , but in return C<Base> can safely be a supertype of C<Derived> .

In “clever words” they say that the class C is covariant in the parameter T , or that T is a covariant type parameter. You can think of C as being a producer of T 's, and NOT a consumer of T 's.

In addition to out , Kotlin provides a complementary variance annotation: in . It makes a type parameter contravariant : it can only be consumed and never produced. A good example of a contravariant type is Comparable :

They also include this code example, which might make things clearer:

interface Comparable<in T> {
    operator fun compareTo(other: T): Int
}

fun demo(x: Comparable<Number>) {
    x.compareTo(1.0) // 1.0 has type Double, which is a subtype of Number
    // Thus, we can assign x to a variable of type Comparable<Double>
    val y: Comparable<Double> = x // OK!
}

I might be misunderstanding what “cast into” means. Can “cast into” mean the same thing as “inherits from”?

1 Like

Sort of. Variance basically adds a new layer on top of regular inheritance which says whether or not we care about what we are getting in or what we are pushing out in relation to that type. In simpler terms, a Producer<out T> has this “inheritance” relationship:

           Producer<Any>
                ^^
               /  \
              /    \
Producer<Number>  Producer<String>
        ^ 
        |
Producer<Int>

where an arrow points to what this value can be safely cast as (i.e. an arrow from Producer<Int> to Producer<Number> means that a Producer<Int> can be safely cast as an Producer<Number> (i.e. you can use a Producer<Int> whenever you need a Producer<Number> with no bad effects. Same way as you can use a String as an Any or an Int as a Number with no bad effects). On the other hand, a Consumer<in T> has this inheritance relationship:

Consumer<Int>
        ^
        |
Consumer<Number>  Consumer<String>
            ^        ^
             \      /
              \    /
           Consumer<Any>

As you can see, in vs out basically flips the whole tree. It all just depends on whether or not you are exposing something that could potentially not follow the type without the compiler screaming in your face. Generics are mostly just used as a way to alert the user to potential bugs at compile time. Another really notable example of this is Kotlin’s List<out T>, which allows you to use a List of Ints as a List of Anys.

That’s correct. But calling it down-casting can sound counter-intuitive without understanding that Consumer<Any> is indeed a subtype of Consumer<String>, thanks to its contra variant type parameter. Hence the former can safely be cast into the later (without ever producing a runtime error).

1 Like

Yeah I see what you mean. I should’ve clarified that I meant downcasting as in downcasting the type parameter itself. Thanks for the tip!!

I have started to understand these covariant and contravariant relationships a bit better with continued online reading. Especially after reading these “guides”:
https://typealias.com/guides/illustrated-guide-covariance-contravariance/
https://typealias.com/guides/ins-and-outs-of-generic-variance/

These guides illustrate the type/subtype relationship with variance. However, they don’t give real life code examples other than use of functions defined in the generic class, so I’m not certain how one would utilize these type/subtype relationships in code otherwise. Would that be done via casting?

You mention “downcasting” and “casting” and I’m now trying to take the type/subtype relationships I am understanding and apply them to casting, but I’m not getting there.

In the code example you provided above:

class Container<in T>(val content: T) //Compiler error right here

fun main() {
    val cont = Container<Any>(Any()) // Just creating a new Any object for this example
    val contString: Container<String> = cont //This is totally legal because in guarantees downcasting safely
    val str: String = contString.content //Whoops, you are now trying to cast an Any to a String.
}

We have setup a simple generic class. We can create objects of that class with an Any type or a String type, or any other type.
We create an object of type Container<Any>, and reference it with the val cont.
It looks like we then cast that object as a type Container<String> and reference that with a different val contString. Is there a type/subtype relationship here?

1 Like

Yes. Container<Any> is a subtype of Container<String>. It’s basically a flipped hierarchy in relations to the type parameter (i.e. Any for example is not a subtype of anything, while String is just a subtype of Any, but when you have a Container of Any, then it is a subtype of every Container. This is because Container has an in type parameter. If you have a different clas tho (called Producer for example) that has an out type parameter, then a Producer of String is a subtype of a Producer of Any i.e the hierarchy is quite similar to the normal type hierarchy)

That’s one definition that keeps coming up that I can’t wrap my head around. I simply cannot see how a Container<Any> can be a subtype of Container<String>. To me that’s like saying a vegetable is a subtype of a carrot, or the solar system is a subtype of Earth.

I know what you mean lol. It’s kinda weird, but think of it as describing the input. If a class (which is called Consumer for the sake of simplicity) can accept all values of type Number, then it can also accept values of type Int, and so a Consumer<Number> can be cast to a Consumer<Int> safely, because the only thing that that Consumer will do with the type is just take it in. Safely casting is synonymous with subtyping i.e. if you can safely cast a B to an A, then B is a subtype of A.

The truth of the matter is that Generics are just a compile-time guarantee. At runtime on the JVM, they are all erased. They just serve as a tool to prevent the programmer from making stupid mistakes like trying to access a List<Any> as a List<String>

I think I understand this “safe casting” now:
a Consumer<Number> can safely be cast to a Consumer<Int> because even though the Consumer<Number> can accept more types than just Int, only the Int will be utilized in this case, so it is safe to use a Consumer<Number> as a Consumer<Int>.

So, in the above code, we are casting cont as a Container<String> into contString, right?

1 Like

But that’s not what we are saying. We are saying that carrot is a subtype of vegetable and therefore a vegetable consumer is a subtype of a carrot consumer.

I think I’ve confused myself again. I know I’m thinking about this too hard.

class Container<in T>(val content: T) //Compiler error right here

fun main() {
    val cont = Container<Any>(Any()) // Just creating a new Any object for this example
    val contString: Container<String> = cont //This is totally legal because in guarantees downcasting safely
    val str: String = contString.content //Whoops, you are now trying to cast an Any to a String.
}

I understand, in this example, the <in> and contravariant relationship. I’m not understanding what the end result of the “casting” is though. We define an object of type Container<Any> and we reference it with a val called cont.
cont has a property called content.
cont.content equals Any() and would be of Any type.

Now, we establish a new val called contString and we reference the same Container<Any> object as cont, but we are… what? … limiting that reference to a type of Container<String>? Like looking at the object through a narrowed window? Does this change <T> at all from <Any> to <String>, so that the content property for contString would of type String now, instead of Any?
More specifically, are cont and contString still referencing the same object, or is contString referencing a different object that was redefined as a String type using the Container<Any> object as a template?

1 Like