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>
:
- we must use
T := Foo
(as T
is invariant)
- we must use
Int
or any of its supertypes as the type argument R
- this is because of covariance for
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 expected
- examples of these supertypes are
Int?
, Number
, Number?
, Any
, Any?
Given that a String
is passed as R
:
- we must use
String
or some of its supertypes as R
- this is necessary to be able to pass a
String
where R
is expected due to (2)
- examples of these supertypes are
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:
Any
. At least this compiles:Foo::bar.test<Foo, Any>("Hello")
, and it doesn't compile withString
orInt
instead ofAny
. – Glenout R
-definition onKProperty1
... if that wasn't there, you would have your expected compile time error when passing aString
... – Rafiqinline infix fun <T, U, R : U> KProperty1<T, U>.test(block : (KProperty1<T, U>) -> R) = block(this)
with a usage as follows:Foo::bar test { 3 }
. This way the compiler will warn you if you return anything that differs fromR : U
... similar things can also be accomplished by using your own intermediate type holding the requested type... (e.g.Foo::bar.toYourIntermediate() test 3
) – Rafiq