I’m working on an expression language based on KProperty instances and I ran across what seems to be a missing check by the type checker:
import kotlin.reflect.KProperty1
fun <T, R> KProperty1<T, R>.eq(x: R): String = "${this.name} eq $x"
class A(val n: Int)
val prop: KProperty1<A, Int> = A::n
val value: String = "foo"
prop.eq(value)
I would expect this code to fail compilation, because the receiver has type parameter R=Int while the argument x has R=String, instead it compiles and run, returning “n eq foo”
when a type parameter T of a class C is declared out, it may occur only in out-position in the members of C, but in return C<Base> can safely be a supertype of C<Derived>.
Thus, KProperty1<A, Any> is a supertype of KProperty1<A, Int>.
As for KProperty<A, Any> the parameter of eq method is Any, it can accept String.
Of course it’s possible to call method of base class on an instance of derived class, so as a result
prop.eq(value)
is inferred to
prop.eq<A, Any>(value)
Note that if the type parameter was contravariant, valid types for method call could still be inferred. Only the invariant version would not compile - see:
class Invariant<T>
class Covariant<out T>
class Contravariant<in T>
inline fun <reified T> Invariant<T>.inferType(x: T): String = "${T::class}"
inline fun <reified T> Covariant<T>.inferType(x: T): String = "${T::class}"
inline fun <reified T> Contravariant<T>.inferType(x: T): String = "${T::class}"
fun main() {
val invariant = Invariant<Int>()
println(invariant.inferType(1)) // kotlin.Int
// println(invariant.eq("foo")) // does not compile!
val covariant = Covariant<Int>()
println(covariant.inferType(1)) // kotlin.Int
println(covariant.inferType("foo")) // kotlin.Any
val contravariant = Covariant<Int>()
println(contravariant.inferType(1)) // kotlin.Int
println(contravariant.inferType("foo")) // kotlin.Any
}
Yes, I don’t think it’s doable with extension method alone.
You could introduce some invariant wrappers for the property classes:
import kotlin.reflect.KProperty1
class A(val n: Int)
inline class InvariantKProperty1<T, V>(private val prop: KProperty1<T, V>)
inline fun <reified T, reified V> InvariantKProperty1<T, V>.inferType(x: V): String = "${T::class}, ${V::class}"
fun <T, V> KProperty1<T, V>.invariant() = InvariantKProperty1(this)
fun main() {
val prop: KProperty1<A, Int> = A::n
println(InvariantKProperty1(prop).inferType(1)) // A, Int
// println(InvariantKProperty1(prop).inferType("foo")) // does not compile!
println(prop.invariant().inferType(1)) // A, Int
// println(prop.invariant().inferType("foo")) // does not compile
}
@akurczak@tobia Going back to the topic now, there is a bug introduced in Kotlin 1.4 where all the examples above will compile if your callsite and extension are in different files (which is always the case in practice). Only 1.3.72 is safe.
So the difference is if it’s inlined as a variable or expression. Looks like compiler will mess it up when calling extension function. Anyway Kotlin 1.4 compiler is full of bugs and you’re better off to stay away from it.