Search code examples
kotlindelegatesimmutabilitymutability

Create a var using a delegate that does not have a setter


I am trying to create delegate var properties with a delegate that does not provide a setValue(...) method. In other words, I need a property that I can reassign but that should get its value via the delegate as long as it hasn't been reassigned.

I am using the xenomachina CLI arguments parser library, which uses delegates. This works well as long as I have val properties. In some cases I need to be able to change those properties dynamically at runtime, though, requiring a mutable var. I can't simply use a var here, as the library does not provide a setValue(...) method in its delegate responsible for the argument parsing.

Ideally, I'd like something like this:

class Foo(parser: ArgParser) {
    var myParameter by parser.flagging(
        "--my-param",
        help = "helptext"
    )
}

which doesn't work due to the missing setter.

So far, I've tried extending the Delegate class with a setter extension function, but internally it also uses a val, so I can't change that. I've tried wrapping the delegate into another delegate but when I do that then the library doesn't recognize the options I've wrapped anymore. Although I may have missed something there. I can't just re-assign the value to a new var as follows:

private val _myParameter by parser.flagging(...)
var myParameter = _myParameter

since that seems to confuse the parser and it stops evaluating the rest of the parameters as soon as the first delegate property is accessed. Besides, it is not particularly pretty.

How do you use delegates that don't include a setter in combination with a var property?


Solution

  • Here is how you can wrap a ReadOnlyProperty to make it work the way you want:

    class MutableProperty<in R, T>(
        // `(R, KProperty<*>) -> T` is accepted here instead of `ReadOnlyProperty<R, T>`,
        // to enable wrapping of properties which are based on extension function and don't
        // implement `ReadOnlyProperty<R, T>`
        wrapped: (R, KProperty<*>) -> T
    ) : ReadWriteProperty<R, T> {
        private var wrapped: ((R, KProperty<*>) -> T)? = wrapped // null when field is assigned
        private var field: T? = null
    
        @Suppress("UNCHECKED_CAST") // field is T if wrapped is null
        override fun getValue(thisRef: R, property: KProperty<*>) =
            if (wrapped == null) field as T
            else wrapped!!(thisRef, property)
    
        override fun setValue(thisRef: R, property: KProperty<*>, value: T) {
            field = value
            wrapped = null
        }
    }
    
    fun <R, T> ReadOnlyProperty<R, T>.toMutableProperty() = MutableProperty(this::getValue)
    
    fun <R, T> ((R, KProperty<*>) -> T).toMutableProperty() = MutableProperty(this)
    

    Use case:

    var lazyVar by lazy { 1 }::getValue.toMutableProperty()
    

    And here is how you can wrap a property delegate provider:

    class MutableProvider<in R, T>(
        private val provider: (R, KProperty<*>) -> (R, KProperty<*>) -> T
    ) {
        operator fun provideDelegate(thisRef: R, prop: KProperty<*>): MutableProperty<R, T> =
            provider(thisRef, prop).toMutableProperty()
    }
    
    fun <T> ArgParser.Delegate<T>.toMutableProvider() = MutableProvider { thisRef: Any?, prop ->
        provideDelegate(thisRef, prop)::getValue
    }
    

    Use case:

    var flagging by parser.flagging(
        "--my-param",
        help = "helptext"
    ).toMutableProvider()