A question about <covariance in Kotlin>: I underst...
# getting-started
h
A question about covariance in Kotlin: I understand that the following class declaration is invalid, because the variable property
state
might be set to a value of type A in one context and then read as a value of type B in another (where A and B are sibling types but not parent/child).
Copy code
class Example<out T>(var state: T) // syntax error
However, method parameters also count as “occuring in the ‘in’ position” (as IntelliJ words it) and I am confused about the specifics, especially as the first code snippet of the following two is not allowed, but the second is:
Copy code
class ConsumerOne<out T> {
    fun consume(value: T) {} // syntax error
}
Copy code
class ConsumerTwo<out T>

fun <T> ConsumerTwo<T>.consume(value: T) {} // allowed
I understand that the latter uses an extension function, which is static and not a member, but that doesn’t explain the difference in covariance to me yet. Specifically, what makes the former unsafe but the latter safe?
e
the extension function is effectively
Copy code
fun <T> consume(this: ConsumerTwo<T>, value: T)
which is fine as a declaration (aside from the name
this
)
then given a value of type
ConsumerTwo<out T>
, you can't actually do anything with it that consumes
in T
h
@ephemient Thanks, that helps. So, as a follow up, what things could you do in a member function that “consume”
in T
but not in an extension, assuming the class has no state? (I think I can make this assumption because such state would also cause a syntax error, and I don’t see yet how a method parameter inherently consumes
T
.)
p
If your class consumes T why not
Copy code
class ConsumerOne<in T>
h
@Pablichjenkov This example is made up for my understanding in general.
👍 1
e
Foo<out>
means that given types
A
and
B
such that
A
is a subtype of
B
,
Foo<A>
is a subtype of
Foo<B>
for parameters, it's always the other way around. given a
fun foo(value: A)
always accepts
foo(value = _some instance of_ B)
thus if you could write
fun <out T> foo(value: T)
(or if you were to do the same inside a
class Example<out T>
), it could be called with any type, and within the function body, no type would be known. this is completely unhelpful so it is forbidden
and on the other hand, if you had a
class Example<in T> { fun foo(): T }
, the function must return a value that is an inhabitant of every type. this does not exist, so that is likewise forbidden
now for free functions (and extensions), the generic type parameters are chosen by the caller. you have some concrete type there, it's not the same case
(unless you put variance on it, which leads you to the same)
p
Adding to what ephemient explained, in general there are 3 types of variance: Invariance, Covariance(out), Contravariance(in). When using <T> there is no variance, so wherever you replace T with an actual type it has to be that type. Invariance can appear in both positions in(input to the class) and out(output of the class) since is only one type invloved. When you use Covariance<out T> you are only allowed to use that T type in output positions(output functions if you will). When using Contravariance<in T> you are only allowed the use T type in position or input functions of that type. These are the java PECS rules Producer(out) Extends, Consumer(in) Super. The Java terminology is a bit confusing to be honest. This is a cool article explaining the variances although the kotlin official documentation explain it very detailed too. https://kt.academy/article/ak-variance-limitations
h
@ephemient I’m tired so maybe I’m misreading, but is your first claim about parameters true? The function
example(string: String)
definitely should not accept any arbitrary
Any
object.
e
it accepts any subtype of
String
(which there aren't any, because
String
is final, but replace it with another type and it'll be more meaningful)
it doesn't accept supertypes of
String
such as
Any
one way to look at
out T
is: if
T
is allowed, so is any supertype of
T
h
Okay, that’s what I thought, but I thought you said foo(value: A) accepts values of type B where B is the supertype of A. And as the following messages are based on that, I don’t understand.
e
e.g.
val list: List<Any> = listOf<String>()
is completely fine: any
String
you can
get()
out
of a
List<String>
is also an
Any
and thus you can see that
out
and
in
cannot operate together: one allows any supertype, one allows any subtype, in subtyping relations
h
I think I get that in general, so I’m not connecting a dot somewhere. It might be helpful to see a minimalist example of a class where type safety could be broken (and such breakage), assuming function parameters were allowed in covariant types.
e
your fails-to-compile example is already an example
h
That example has an empty body, but let’s say instead it printed
value
with
println
. So
Copy code
class ConsumerOne<out T> {
    fun consume(value: T) { // syntax error
        println(value)
    }
}
As far as I know, this doesn’t cause any issues since it just does a simple print, and there’s no properties in
ConsumerOne
so I can’t have issues there; what kind of things could be done in the body instead (things that would not be allowed in arbitrary code, like an extension)?
e
Copy code
open class ConsumerOne<out T> {
    open fun consume(value: T) {} // if this were not rejected, then
}

class IntConsumer : ConsumerOne<Int> {
    open fun consume(value: Int) { println(value + 1) }
}

val consumer: ConsumerOne<Any> = IntConsumer() // permitted, because ConsumerOne<Int> is a subtype of ConsumerOne<Any>, due to <out>

consumer.consume("") // permitted, because String is a subtype of Any
then
consume(value: Int)
would be called with an
String
, which is broken
h
Can this be achieved without making ConsumerOne
open
?
e
yes, it is just clearer to illustrate this way
h
I ask because I was thinking “maybe variance errors could be relaxed for types that do not support inheritance”, but obviously if that was the case, it surely would have been done by now. So if I could see such an example I think I would get it. And thanks for your help btw
e
Copy code
class Consumer<out T>(val consume: (T) -> Unit) // will not compile, but if it did

val consumer: Consumer<Any> = Consumer<Int> { value: Int -> println(value + 1) }

consumer.consume("")
h
Well but that’s a function property, which is essentially open in that the function body can change across instances. For a closed, non-extendable function like
consume
in the original example, what could be done?
e
it doesn't have to be a function property, it can be any other bit of state that uses
T
your
println
doesn't expose it because it is equivalent to
Copy code
class Consumer {
    fun consume(value: Any?) { println(value) }
}
under erasure, and none of the generic typing gives you any additional behavior over that
h
The only state I can think of that isn’t a property would be global state, and as I understand that global state could just as easily be set in the extension function. (I’m going to bed soon so maybe I’ll realize in the morning that I was missing some obvious thing, but thank you again lol.)