Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

Kotlin - Generic Type Parameters Not Being Respected

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?

like image 357
Matthew Layton Avatar asked Aug 01 '19 23:08

Matthew Layton


1 Answers

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:

like image 126
hotkey Avatar answered Nov 08 '22 17:11

hotkey