While writing this, I noticed that the Bug I referenced in my comments has been fixed, so I continued down that code path only to realize, there is a language limitation, at the bottom I will include an example of what I meant in the comments.
Both of these examples would be failures at runtime.
fun test_person() {
val village = village {
house {
person {
name = "Emily"
// ::name setTo "Emily" // Commented for 2nd example of Person
age = 31
}
person {
name = "Jane"
age = 19
}
}
house {
person {
name = "Tim"
// name = "Tom" // Will break with exception
age = 20
}
}
}
println("What is our village: \n$village")
}
The runtime breaking example that works with exceptions:
class Village {
val houses = mutableListOf<House>()
fun house(people: House.() -> Unit) {
val house = House()
house.people()
houses.add(house)
}
override fun toString(): String {
val strB = StringBuilder("Village:\n")
houses.forEach { strB.append(" $it \n") }
return strB.toString()
}
}
fun village(houses: Village.() -> Unit): Village {
val village = Village()
village.houses()
return village
}
class House {
val people = mutableListOf<Person>()
fun person(qualities: Person.() -> Unit) {
val person = Person()
person.qualities()
people.add(person)
}
override fun toString(): String {
val strB = StringBuilder("House:\n")
people.forEach{ strB.append(" $it \n")}
return strB.toString()
}
}
class Person {
var age by SetOnce<Int>()
var name by SetOnce<String>()
override fun toString(): String {
return "Person: { Name: $name, Age: $age }"
}
}
class SetOnce <T> : ReadWriteProperty<Any?, T?> {
private var default: T? = null
override fun getValue(thisRef: Any?, property: KProperty<*>): T? = default
override fun setValue(thisRef: Any?, property: KProperty<*>, value: T?) {
if (default != null) throw Exception("Duplicate set for ${property.name} on $thisRef")
else default = value
}
}
The Non-working example that was intended to use lateinit properties to control the setting of a value once, but you cannot use a reference, it must be the literal ::foo
syntax. Like I said, I didn't realize that bug was fixed, and did not know this was a limitation in the language
class Person {
lateinit var name: String
lateinit var age: Number // Number because primitives can't be lateinit
/** @throws Exception when setting a property a second time */
infix fun <T> KMutableProperty0<T>.setTo(value: T) {
val prop = getProp<T>(this.name)
if (prop.isInitialized.not()) this.set(value)
else throw Exception("Duplicate set for ${this.name}")
}
private fun <T> getProp(name: String): KMutableProperty0<T> {
return when(name) {
"name" -> ::name
"age" -> ::age
else -> throw Exception("Non-existent property: $name")
} as KMutableProperty0<T>
}
}
As contracts mature and the rules are relaxed, we could potentially write something like:
@OptIn(ExperimentalContracts::class)
infix fun <T> KMutableProperty0<T>.setTo(value: T) {
contract { returns() implies this@setTo.isInitialized }
this.set(value)
}
Which would give us the capability to move this all to IDE errors, but we're not there yet sadly.