Chaining property accessors and maintaining both the ability to read and write

Using a KMutableProperty1 to access a classes property works both as a getter and setter.

class BaseClass(
    var baseInt: Int = 0,
    var baseInnerClass: InnerClass = InnerClass()
)

class InnerClass(
    var innerInt: Int = 0,
)

val target = BaseClass()

val kMutableProperty1b = (BaseClass::baseInt)
kMutableProperty1b.set(target, 4)
val baseInt = kMutableProperty1b.get(target)

To be able to access nested properties like

BaseClass::innerClass -> InnerClass:innerInt

I tried to up chain two kMutableProperty1 with

fun <A, B, C> ((A) -> B).chained(getter : (B) -> C) : (A) -> C = { getter(this(it)) }

With that, the inner properties can be read, but not set:

val chainedKMutableProperty = baseMutableProperty.chained(InnerClass::innerInt)
val innerInt = chainedKMutableProperty(target)
chainedKMutableProperty.set(target, 5) // Not available

In Swift something similar can be achieved using KeyPaths

let target = BaseClass()

let aKeyPath = \BaseClass.baseInt
target[keyPath: aKeyPath] = 4
let baseInt = target[keyPath: aKeyPath]

let bKeyPath = \BaseClass.baseInnerClass
let chainedKeyPath = bKeyPath.appending(path: \InnerClass.innerInt)

let innerInt = target[keyPath: chainedKeyPath]

target[keyPath: chainedKeyPath] = 5

How can I do the same in Kotlin - chaining property accessors and maintaining both the ability to read and write?

I don’t think there is something like this already in Kotlin or Java stdlib. We can easily create it by ourselves, although I don’t think it is a good idea to stick to KProperty. This interface isn’t just a generic accessor interface. It is a very specific thing: a property of a class.

Instead, I suggest to create our own interfaces. Below is a simple POC:

fun main() {
    val target = BaseClass()

    val chainedProp = BaseClass::baseInnerClass chain InnerClass::innerInt
    println(chainedProp.get(target))
    chainedProp.set(target, 5)

    // or
    println(target[chainedProp])
    target[chainedProp] = 12
}

operator fun <T, V> T.get(key: MyProperty<T, V>): V = key.get(this)
operator fun <T, V> T.set(key: MyMutableProperty<T, V>, value: V) = key.set(this, value)

infix fun <T, V, V2> KProperty1<T, V>.chain(next: KMutableProperty1<V, V2>): MyMutableProperty<T, V2> = asMyProperty() chain next.asMyProperty()

infix fun <T, V, V2> MyProperty<T, V>.chain(next: MyMutableProperty<V, V2>): MyMutableProperty<T, V2> = object : MyMutableProperty<T, V2> {
    override fun get(receiver: T): V2 {
        return next.get(this@chain.get(receiver))
    }

    override fun set(receiver: T, value: V2) {
        next.set(this@chain.get(receiver), value)
    }
}

fun <T, V> KProperty1<T, V>.asMyProperty(): MyProperty<T, V> = object : MyProperty<T, V> {
    override fun get(receiver: T): V {
        return this@asMyProperty.get(receiver)
    }
}

fun <T, V> KMutableProperty1<T, V>.asMyProperty(): MyMutableProperty<T, V> = object : MyMutableProperty<T, V> {
    override fun get(receiver: T): V {
        return this@asMyProperty.get(receiver)
    }

    override fun set(receiver: T, value: V) {
        this@asMyProperty.set(receiver, value)
    }
}

interface MyProperty<in T, out V> {
    fun get(receiver: T): V
}

interface MyMutableProperty<in T, V> : MyProperty<T, V> {
    fun set(receiver: T, value: V)
}
1 Like

Wow… thats awesome! Thank a lot!

I changed chain to an operator invoke to be able to write the chain as
val chainedProp = (BaseClass::baseInnerClass)(InnerClass::innerInt)

and added a third invoke to be able to chain more than two blocks.

Thanks again! :slight_smile:

infix operator fun <T, V, V2> KProperty1<T, V>.invoke(next: KMutableProperty1<V, V2>): MyMutableProperty<T, V2> = asKeyPath() invoke next.asKeyPath()

infix operator fun <T, V, V2> MyProperty<T, V>.invoke(next: KMutableProperty1<V, V2>): MyMutableProperty<T, V2> = this invoke next.asKeyPath()

infix operator fun <T, V, V2> MyProperty<T, V>.invoke(next: MyMutableProperty<V, V2>): MyMutableProperty<T, V2> = object : MyMutableProperty<T, V2> {
    override fun get(receiver: T): V2 {
        return next.get(this@invoke.get(receiver))
    }

    override fun set(receiver: T, value: V2) {
        next.set(this@invoke.get(receiver), value)
    }
}
1 Like

You’ll probably need more functions like this. For example, you need a way to chain not mutable properties (on the right side). So probably another 3 versions of this function. But this is a good start :slight_smile:

After changing to operator invoke I suggest removing infix - it doesn’t make too much sense anymore.

1 Like

Oh right, the infix keyword is not needed anymore :smiley: I’ve just got to know it from your code. Didn’t encounter it before - but thats a cool feature to make code more readable.