Generic extensions of KProperty1 in Kotlin

1k Views Asked by At

I have the following code:

import kotlin.reflect.KProperty1


infix fun <T, R> KProperty1<T, R>.eq(value: R) {
    println(this.name + " = $value")
}

infix fun <T, R> KProperty1<T, R>.eq(value: KProperty1<T, R>) {
    println(this.name + " = " + value.name)
}

data class Person(val age: Int, val name: String, val surname: String?)

fun main() {
    Person::age eq 1  // Valid. First function
    Person::name eq "Johan"  // Valid. First function
    Person::name eq Person::name  // Valid. Second function
    Person::age eq 1.2f  // Valid, but it shouldn't be. First function
    Person::age eq Person::name  // Valid, but it shouldn't be. First function
    Person::surname eq null  // Invalid, but it should be. Second function, but it should be first
}

I am trying create extension functions for the KProperty1 class with generics, but my generics are not matching as I'd expect. main() lists some uses of the two functions, with 3 unexpected results at the end and a description of what I should expect.

1

There are 1 best solutions below

0
On BEST ANSWER

First, note that KProperty1<T, out R> has an out-projected type parameter R, so an instance of KProperty1<Foo, Bar> can also be used where a KProperty1<Foo, Baz> is expected, where Baz is a supertype of Bar, such as Any.

This is exactly what happens when you call eq with types that don't quite match: a more common supertype is used so that the call resolves correctly.

For example, this call:

Person::age eq 1.2f

is effectively equivalent to:

Person::age.eq<Person, Any>(1.2f)

You can even transform it to this form automatically be applying Replace infix call with ordinary call and then Add explicit type arguments.

So whenever the types don't match exactly, a common supertype is chosen for R. Currently, there's no proper way to tell the compiler it should not fall back to the supertype (see this Q&A).

The last call is a side effect of how type inference currently works: it seems like the compiler has to choose one of the overloads before it decides on whether it is compatible for nullability. If you replace null with (null as String?) it works. Please file an issue for this at kotl.in/issue.

In general, it is preferable not to mix the generic signatures with non-generic ones because of possible generic substitutions that lead to ambiguity. In your case, a substitution R := KProperty1<T, Foo> (i.e. using eq on a property of this type) would lead to ambiguity.

Also relevant: