Consider the following example:
import kotlin.reflect.KProperty1
infix fun <T, R> KProperty1<T, R>.test(value: R) = Unit
data class Foo(val bar: Int)
fun main() {
Foo::bar test "Hello"
}
Given that test
expects a value
of type R
, why in this context, where the property type is Int
, does it allow me to pass a String
?
First, take a look at the declaration of the interface KProperty1
, which is:
interface KProperty1<T, out R> : KProperty<R>, (T) -> R
The important part here is the out
-projected type parameter R
, which defines subptyping relationships between the KProperty1
types with different type arguments used for R
.
(1) Namely, for any Foo
, A
and B
such that A : B
(A
is a subtype of B
), KProperty1<Foo, A> : KProperty1<Foo, B>
. This is called covariance, because the parameterized types relate to each other in the same way as their type arguments do.
(2) Next, note that for any A
and B
such that A : B
, an instance of A
can be passed as an argument to any B
-typed parameter. Receiver parameters of extension functions are not different from normal parameters in this respect.
Now, the crucual part is the type inference algorithm that the compiler runs. One of the goals of type inference is to establish statically-known type arguments for each generic call where type arguments are omitted.
During type inference for the call Foo::bar test "Hello"
, the compiler needs to actually infer the type arguments for T
and R
based on the known types of the receiver Foo::bar
(KProperty1<Foo, Int>
) and the value
argument "Hello"
(String
).
This is done internally by solving a constraint system. We could emulate this logic as follows:
Given that KProperty<Foo, Int>
is passed as KProperty<T, R>
:
T := Foo
(as T
is invariant)Int
or any of its supertypes as the type argument R
R
: given (1) and (2) combined, choosing Int
or some of its supertypes for R
is necessary to be able to pass KProperty<Foo, Int>
where KProperty<Foo, R>
is expectedInt?
, Number
, Number?
, Any
, Any?
Given that a String
is passed as R
:
String
or some of its supertypes as R
String
where R
is expected due to (2)
String?
, CharSequence
, CharSequence?
, Any
, Any?
Given the two constraints on R
, namely that it should be Int
or some of its supertypes and it should be String
or some of its supertypes, the compiler finds the least common type that satisfies both. This type is Any
.
So, the inferred type arguments are T := Foo
and R := Any
, and the call with explicit type arguments would be:
Foo::bar.test<Foo, Any>("Hello")
In IntelliJ IDEA, you can use an action Add explicit type arguments on a non-infix call to add the inferred types.
Disclaimer: this is not exactly how the compiler works internally, but using this way of reasoning you may often get the results that agree with the compiler's results.
Also relevant:
If you love us? You can donate to us via Paypal or buy me a coffee so we can maintain and grow! Thank you!
Donate Us With