2

So I'm trying to define a method like this


fun <R,F> myFunction(prop: KProperty1<R, F>, value:F)  {}


// so that the compiler only allows me to invoke it like 
myFunction(User::name, "Alejandro")
// and stops developers from doing illegal things like
myFunction(User::name, 123)

//However, compiler doesn't complain if I do that... it widens the type to Any

How can I achieve that?

caeus
  • 3,084
  • 1
  • 22
  • 36
  • related: https://stackoverflow.com/q/39596420/4161471 https://stackoverflow.com/q/39812227/4161471 – aSemy Sep 09 '22 at 18:02
  • KMongo achieves it using an annotation on the type param, but I don't see what it does. – caeus Sep 09 '22 at 18:17
  • 2
    Out of curiosity, what is a use case for this? I can think of things you might want to do with a KMutableProperty and item of matching return type, but not really for a read-only property. Anyway, the Checker class solution in the 2nd link above could be adapted for your situation here. – Tenfour04 Sep 09 '22 at 18:56
  • The annotation that KMongo uses is `@kotlin.internal.OnlyInputTypes`. There are more details in KT-13198, including [how to access it](https://youtrack.jetbrains.com/issue/KT-13198/#focus=Comments-27-5102119.0-0). It doesn't look like it will be released into stdlib any time soon. – aSemy Sep 09 '22 at 22:27
  • @Tenfour04 typesafe query builder – caeus Sep 12 '22 at 20:42

1 Answers1

1

Kotlin is "widening" the type here because the value type parameter (i.e. the second type parameter) of KProperty1 is defined with keyword out which makes that parameter covariant.

This means that for instance KProperty1<User, String> is a subtype of KProperty1<User, Any>, and hence User::name which is presumably a KProperty1<User, String>, can also be seen as a special case of KProperty<User, Any>. Therefore, it is totally legal to call myFunction<User,Any>(User::name, 123).

The logic behind this can be derived from the name of the out keyword: It is expected that the typed parameter is only used in "out position" of any function call. In the case of KProperty1 this makes sense, because it is the type of the return value of the property. When you get a value from a KProperty1<K, V>, that value is of type V and thus it can be used anywhere where it is okay to have some supertype of V.

This should only be a problem, if you want to use the value in the "in position" of some function, for instance, if you want to write a function that takes a value of type V and store it in a KProperty1<K, V>.

If this is what you want, you are lucky, because you can and should just use KMutableProperty1<K,V> where the value parameter does not have an out keyword which means that it is invariant. Also, that interface allows you to put the value into the property.

Changing your function definition to

fun <R,F> myFunction(prop: KMutableProperty1<R, F>, value:F)  {}

makes that the compiler allows myFunction(User::name, "Alejandro"), but it complains on myFunction(User::name, 123).

See also: Kotlin documentation on Variance

Karsten Gabriel
  • 3,115
  • 6
  • 19
  • I get that variance is the reason it happens. Somehow KMongo forces KProperty to be invariant at call site using some sort of magic (https://litote.org/kmongo/typed-queries/) (https://github.com/Litote/kmongo/blob/8e1496a0ff787e0c188677ef91dfa35f462293d6/kmongo-property/src/main/kotlin/org/litote/kmongo/Filters.kt#L40). It is also mentioned here (https://stackoverflow.com/questions/47191898/force-type-parameter-to-be-invariant-at-use-site-when-it-is-covariant-at-declara) that is is possible somehow... – caeus Sep 09 '22 at 22:02