Kotlin - Generic Type Parameters Not Being Respected
Asked Answered
S

1

7

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?

Supersonic answered 1/8, 2019 at 23:27 Comment(4)
Most likely it's inferred to be Any. At least this compiles: Foo::bar.test<Foo, Any>("Hello"), and it doesn't compile with String or Int instead of Any.Glen
your problem seems to be related with the out R-definition on KProperty1... if that wasn't there, you would have your expected compile time error when passing a String...Rafiq
I think the first sentence in https://mcmap.net/q/1621842/-generic-extensions-of-kproperty1-in-kotlin answers your questionDitter
as a workaround (but there are probably nicer ways to support what you want), you may just pass the kproperty along to a producer-function, e.g.: inline 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 from R : 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
O
8

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:

Offprint answered 5/8, 2019 at 13:25 Comment(0)

© 2022 - 2024 — McMap. All rights reserved.