Missing type check?

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”

What am I missing here?

I tried both Kotlin 1.3.72 and 1.4.0

2 Likes

I believe this is expected and correct behavior.

eq function is a generic function, so i’ts actual type parameters can be either specified at the call site or inferred if possible.

`KProperty`` is defined as follows:

interface KProperty1<T, out V>

Note the V parameter is covariant. As stated in Kotlin Generics docs:

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
}

I see. So there is no way to add an extension method on properties and require the argument to be the same type as the property itself?

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
}