I've been recently learning Kotlin, while having some questions with covariant type.
The sample code is here.
I have Option and Option2 both having a type parameter T and a run extension.
I could understand the first two run in validation(), since they are behaved as Java.
But why does the third line compile? Option<T> is invariant in T. We cannot passing Option<C> instance into where Option<B> is expected.
After I add an out keyword for T, now all of them could compile. Why?
open class A
open class B : A()
open class C : B()
class Option<T>(val item: T)
fun <T> Option<T>.run(func: (Int) -> Option<T>): Option<T> = func(1)
class Option1<out T>(val item: T) //out keyword
fun <T> Option1<T>.run(func: (Int) -> Option1<T>): Option1<T> = func(1)
fun validation() {
val opt: Option<B> = Option(B())
opt.run { Option(A()) } //won't compile as expected
opt.run { Option(B()) } //return type is Option<B>
opt.run { Option(C()) } //return type is Option<B>; why could this compile?
val opt1: Option1<B> = Option1(B())
opt1.run { Option1(A()) } //return type is Option<A>; why could this compile?
opt1.run { Option1(B()) } //return type is Option<B>
opt1.run { Option1(C()) } //return type is Option<B>
}
Here, you can approximate the behavior as follows by decomposing the call into the two lines that are type-checked separately:
The first line is correct because:
Option<B>(with exactlyB, asOptionis invariant),Option(item: T): Option<T>constructor call needs to accept aB,C(),C : B,C()passes the check for beingB,Option(C())can also be typed asOption<B>and passes the check,(Int) -> Option<B>.Sanity check: what if you replace the first line as follows?
Then it won't get compiled, as the expression inside the lambda is now typed as
Option<C>which is not a subtype ofOption<B>.In this sample, the type that the compiler chose for
Tis notB, it isA. The compiler is allowed to do that because of covariance of the type parameterT.opt1isOption1<B>Option1<out T>is covariant onT, which allows substitutingTwith any supertype ofB,This is allowed because for any
Zsuch thatB : Z,opt1can also be treated asOption1<out Z>thanks to theoutmodifier, and the compiler can then type-check the call against a receiver typeOption1<Z>.the substition for
Twould be the least common supertype ofBand whateverXsuch that the lambda returnsOption1<X>,Option1<A>,BandA,B : A, the least common supertype isAT := A.Sanity check: what if you change the expression as follows?
It will still compile successfully, but the inferred return type will be
Option1<Any>. This is totally reasonable according to the above, because the least common supertype ofBandIntisAny.Disclaimer: this is not how the compiler works internally, but using this way of reasoning you may often get the results that agree with the compiler's results.