Property delegate class idea

Sometimes I happen to define a class / object that is only used to store delegated properties in such manner (simplified):

class LazyStuff {
  val someNum by lazy { 42 }
  val someString by lazy { "Hello" }
  val someImage by lazy { BufferedImage(4096, 4096, TYPE_INT_ARGB) }
  val someData by lazy { (0..999999).map { it*it } }
}

These tend to be really large, and very repetitive. I was thinking, wouldn’t it be more convenient to write such classes as follows (just the idea):

delegate class LazyStuff by fun <T> kotlin.lazy(initializer: () -> T) {
  someNum { 42 }
  someString { "Hello" }
  someImage { BufferedImage(4096, 4096, TYPE_INT_ARGB) }
  someData { (0..999999).map { it*it } }
}

Of course this requires more thought and be probably designed better (val / var usage, explicit type specification etc.)

What do think that about it?

I understand that this is just an example, but just to be sure…
The num and string and maybe the bufferedimage (don’t know about its implementation) shouldn’t be lazy.
Look at the byte code to see why.

I don’t like the idea because it seems like you are invoking something that doesn’t exist…
Thereby, I can’t see if it’s a var/val.
When you do want to add both explicitly, you and up at the current delegated properties.

Inheriting from an abstract class or a trait (class + interface with delegation) allows you to define a protected fun invoke which makes the lazy-words unneeded.
Given that you specify the var/val, you will save 4 characters per line and for that you will safe a complete language - construct.

Having said that…

When you do as it this way, you could create great classes with pure dsls inside it…

Ok, I thought about the syntax a bit, and that’s how I would see it:

delegate class LazyStuff <T> by { init: (() -> T) -> lazy(init) } {
  delegate val someNum by { 42 }
  delegate val someString by { "Hello" }
  delegate val someImage by { BufferedImage(4096, 4096, TYPE_INT_ARGB) }
  delegate val someData by { (0..999999).map { it*it } }
}

Which would be equivalent to:

final class LazyStuff {
  
  private fun <T> _delegateFactory(init: ()->T): Lazy<T> {
    return kotlin.lazy(init)
  }
  
  val someNum: Int by _delegateFactory { 42 }
  val someString: String by _delegateFactory { "Hello" }
  val someImage: BufferedImage by _delegateFactory { BufferedImage(4096, 4096, TYPE_INT_ARGB) }
  val someData: List<Int> by _delegateFactory { (0..999999).map { it*it } }
  
}

As previous examples were pretty simple, let me present a bit more complex one:

object DataStorage {
  
  private val data: MutableMap<String, Any?> = mutableMapOf()

  fun store(key: String, value: Any?) {
    data[key] = value
  }

  fun <T> get(key: String): T? = data[key] as T?
  
}

class DataStorageDelegate <T>(private val dataStorage: DataStorage, private val defaultValue: T? = null) {
  
  operator fun getValue(thisRef: Any, prop: KProperty<*>): T? {
    return dataStorage.get(prop.name) ?: defaultValue
  }

  operator fun setValue(thisRef: Any, prop: KProperty<*>, value: T?) {
    dataStorage.store(prop.name, value)
  }
  
}

delegate class DataAccessor <T> (dataStorage: DataStorage) by {
  defaultValue: T? -> DataStorageDelegate(dataStorage, defaultValue)
} {
  delegate var configurationJson: String?
  delegate var logoImage: BufferedImage?
  delegate var welcomeMessage: String? by "Hello"
}

Where the last class would translate to::

final class DataAccessor(private val dataStorage: DataStorage){

  private fun <T> _delegateFactory(defaultValue: T?): DataStorageDelegate<T> {
    return DataStorageDelegate(dataStorage, defaultValue)
  }

  var configurationJson: String? by _delegateFactory(null)
  var logoImage: BufferedImage? by _delegateFactory(null)
  var welcomeMessage: String? by _delegateFactory("Hello")

}

Notice that we get rid of private val dataStorage reference in the class constructor, and that encourages encapsulation. Also I proposed to be able to omit by xxx, if one wants to pass null, but that’s optional.

I propose that we could pass delegate factory as follows:

  • existing method (as in original example) - need to specify generics and proper overload
  • lambda (DataStorage example)
  • anonymous function - with special exception to allow default arguments

Below you can see three examples what you can do today.
Because I don’t see a big improvement compared with your code, I personally don’t think it’s a good idea.
(My other reason is that I have idle hope that Kotlin somedays will improve the interface delegation…)

Use trait

interface DataDelegateBuilder {
    fun <T> del(defaultValue: T? = null): DataStorageDelegate<T>
}

class DataAccessor<T>(
    dataStorage : DataStorage
) : DataDelegateBuilder by object : DataDelegateBuilder {
    override fun <T : Any?> del(defaultValue: T?) 
        = DataStorageDelegate(dataStorage, defaultValue)
} {
    var configurationJson by del<String?>()
    var logoImage by del<BufferedImage?>()
    var welcomeMessage by del<String?>("Hello")
}

use extensions

interface DataDelegateBuilder {
    operator fun <T> DataStorage.invoke(defaultValue : T? = null) 
        = DataStorageDelegate(this, defaultValue)
}

// in kotlin
class DataAccessor<T>(dataStorage : DataStorage) : DataDelegateBuilder{
    var configurationJson : String? by dataStorage()
    var logoImage : BufferedImage? by dataStorage()
    var welcomeMessage : String? by dataStorage("Hello")
}

overkill (?) :

interface DataDelegateBuilder {
    operator fun <T> DataStorage.invoke(defaultValue : T? = null) = DataStorageDelegate(this, defaultValue)
    operator fun <T> DataStorage.getValue(thisRef: Any, prop: KProperty<*>): T? {
        return get(prop.name)
    }

    operator fun <T> DataStorage.setValue(thisRef: Any, prop: KProperty<*>, value: T?) {
        store(prop.name, value)
    }
}

class DataAccessor<T>(dataStorage : DataStorage) : DataDelegateBuilder{
    var configurationJson : String? by dataStorage
    var logoImage : BufferedImage? by dataStorage
    var welcomeMessage : String? by dataStorage("Hello")
}