Hello, I am confused by Kotlin type system, how do...
# getting-started
d
Hello, I am confused by Kotlin type system, how does this compile?
Copy code
fun <Class, Value> store(property: KProperty1<Class, Value>, value: Value) {

}

data class Test(
    val name: String,
)

store(Test::name, 1) // This should be illegal cause name is String no?
d
I guess it's because the
Value
is inferred to
Any
. I recently stumbled upon that myself and wondered whether there's an easy way to see the inferred types in these kinds of situations.
d
Oh, that makes sense, thank you. Is there a way to prevent that?
c
In IntelliJ, you can invoke "Type info" (CTRL SHIFT P by default) to get information on the type of what you selected. I added
Value
as a return of your original function to force IntelliJ to see it.
So it wasn't inferred to
Any
(the compiler found something slightly more specific), but for all practical purposes that is indeed what is happening.
Is there a way to prevent that?
I don't see a way for this example. Maybe if you have a more complete case of what you are trying to do?
a
you can kind of make it work (or rather, make it not work!) with property delegates, but it's super hacky - don't do this! It's probably better if you move the delegates into your Test class https://pl.kotl.in/0HAfGVhGY
d
@CLOVIS I have, but it gets pretty complex. Generally I am trying to create a filter for list of objects, so I can use it from a interface and users of that interface can implement the filtering as needed. I took inspiration from a Modifier in compose. So the original idea was something like this:
Copy code
val filter: Filter<Test> = Filter
    .where(Test::name, equals = "Some name")
    .where(Test::number, equals = 17)
I had to change it a bit, but I have this:
Copy code
// Can filter only the types that implement this
sealed interface ServiceData<out This> where This: ServiceData<This>

sealed interface AnyFilter<out Data : ServiceData<Data>>

class Filter<out Data : ServiceData<Data>> : AnyFilter<Data>

data class CombinedFilter<Data: ServiceData<Data>>(
    val filter1: AnyFilter<Data>,
    val filter2: AnyFilter<Data>,
) : AnyFilter<Data>

data class EqualsFilter<Data : ServiceData<Data>, out Value>(
    val field: KProperty1<Data, Value>,
    val value: Value,
) : AnyFilter<Data>

fun <Data : ServiceData<Data>, Value> AnyFilter<Data>.where(field: KProperty1<Data, Value>, equals: Value): AnyFilter<Data> =
    CombinedFilter(this, EqualsFilter(field, equals))

fun <Data: ServiceData<Data>> processFilter(type: KClass<Data>, filter: AnyFilter<Data>) {
        return when (filter) {
            is Filter -> { }
            is CombinedFilter<Data> -> {
                processFilter(type, filter.filter1)
                processFilter(type, filter.filter2)
            }
            is EqualsFilter<Data, *> -> {
                /* 
                    Send to somewhere for comparison: filter.field.name, filter.value (I would actually wanna get the SerialName here but this is the best I can do)  
                */ 
            }
        }
    }
And it then do:
Copy code
data class Test(
    val name: String,
    val number: Int,
) : ServiceData<Test>

processFilter(Test::class, filter = Filter<Test>()
    .where(Test::name, "Some name")
    .where(Test::number, 17)
)
Now that I have written it, I dont think its really much relevant, but I dont wanna discard the post now
c
Another alternative:
Copy code
import kotlin.reflect.KProperty1

data class Foo(
    val name: String,
)



@JvmInline value class PropertyWrapper<T>(val property: KProperty1<*, T>) {
    infix fun store(value: T) {
        TODO("your original function here")
    }
}
fun <T> into(property: KProperty1<*, T>) = PropertyWrapper(property)




into(Foo::name) store 1 // does not compile
It works by binding the type parameter to the value in a first function. When the second function is called, the type parameter is already bound, so the compiler cannot infer anything else.
Maybe this version is a bit nicer-looking (same idea):
Copy code
@JvmInline value class PropertyWrapper<T>(val property: KProperty1<*, T>) {
    infix fun set(value: T) {
        TODO("your original function here")
    }
}
val <T> KProperty1<*, T>.storage get() = PropertyWrapper(this)


Foo::name.storage set "test"
Foo::name.storage set 1
🧠 2
d
Yeah, thats actually really promising
c
Looking at your example, you could probably get something like this working:
Copy code
Filter
    .where(Foo::name) isEqualTo yourValue
where the
where
can emit a similar kind of wrapper
d
Why do I need to use infix? I would rather have something like this:
Copy code
.where(Foo::name.storage, equals = "Some")
c
Why do I need to use infix?
You don't, I just find it nicer 🙂 What you have to do is have a first function that binds the type parameter into its return type. When the second function is called, it is bound already, so it cannot be up-casted.
d
@CLOVIS I didn't know about the IntelliJ shortcut so thanks for that! However, my context was exactly a function without that return type, namely
assertEquals
. Is there any way how to see what the compiler thinks in such scenarios?
c
Btw all of this is because the type parameter of
KProperty1
is
out
, so
KProperty1<Foo, Int>
is a subtype of
KProperty1<Foo, Any>
. My trick is simply to use a wrapper that is invariant, such that they are not subtype of each other anymore.
Copy code
.where(Foo::name.storage, equals = "Some")
I think this would work? I'll let you try it
d
@CLOVIS I played with it a little bit, I don't think this is possible
Copy code
.where(Foo::name.storage, equals = "Some")
But im gonna stick with something like this:
Copy code
.where(Foo::name.value equalTo "Some")
c
Can you show your code for the first version? Ideally via a playground link. I'm curious why it doesn't work.
d
Sure https://pl.kotl.in/dIT9H-yKA, I think I would have to pass the
value
directly to the instance of a wrapper created from the
KProperty1
. And that is not possible to do inside a single function
Anyway, thank you very much, it helped me a lot
a
You can also use the @Exact annotation to accomplish this, however it is an internal kotlin annotation so you'll need to suppress visibility errors
Copy code
@Suppress("INVISIBLE_MEMBER", "INVISIBLE_REFERENCE")
fun <Class, Value> store(property: KProperty1<Class, @kotlin.internal.Exact Value>, value: Value) {

}

data class Test(
    val name: String,
)

fun main() {
    store(Test::name, "hello") // Compiles
    store(Test::name, 11) // Compile Error: Type mismatch
}
d
Ok, hey that is exactly what I had in mind. Is it safe to use? Im on multiplatform btw, but the store function is actually
internal
itself