How can I set the JsName for a property's backing field in Kotlin? - kotlin

I played about with Kotlin's unsupported JavaScript backend in 1.0.x and am now trying to migrate my toy project to 1.1.x. It's the barest bones of a single-page web app interfacing with PouchDB. To add data to PouchDB you need JavaScript objects with specific properties _id and _rev. They also need to not have any other properties beginning with _ because they're reserved by PouchDB.
Now, if I create a class like this, I can send instances to PouchDB.
class PouchDoc(
var _id: String
) {
var _rev: String? = null
}
However, if I do anything to make the properties virtual -- have them override an interface, or make the class open and create a subclass which overrides them -- the _id field name becomes mangled to something like _id_mmz446$_0 and so PouchDB rejects the object. If I apply #JsName("_id") to the property, that only affects the generated getter and setter -- it still leaves the backing field with a mangled name.
Also, for any virtual properties whose names don't begin with _, PouchDB will accept the object but it only stores the backing fields with their mangled names, not the nicely-named properties.
For now I can work around things by making them not virtual, I think. But I was thinking of sharing interfaces between PouchDoc and non-PouchDoc classes in Kotlin, and it seems I can't do that.
Any idea how I could make this work, or does it need a Kotlin language change?

I think your problem should be covered by https://youtrack.jetbrains.com/issue/KT-8127
Also, I've created some other related issues:
https://youtrack.jetbrains.com/issue/KT-17682
https://youtrack.jetbrains.com/issue/KT-17683
And right now You can use one of next solutions, IMO third is most lightweight.
interface PouchDoc1 {
var id: String
var _id: String
get() = id
set(v) { id = v}
var rev: String?
var _rev: String?
get() = rev
set(v) { rev = v}
}
class Impl1 : PouchDoc1 {
override var id = "id0"
override var rev: String? = "rev0"
}
interface PouchDoc2 {
var id: String
get() = this.asDynamic()["_id"]
set(v) { this.asDynamic()["_id"] = v}
var rev: String?
get() = this.asDynamic()["_rev"]
set(v) { this.asDynamic()["_rev"] = v}
}
class Impl2 : PouchDoc2 {
init {
id = "id1"
rev = "rev1"
}
}
external interface PouchDoc3 { // marker interface
}
var PouchDoc3.id: String
get() = this.asDynamic()["_id"]
set(v) { this.asDynamic()["_id"] = v}
var PouchDoc3.rev: String?
get() = this.asDynamic()["_rev"]
set(v) { this.asDynamic()["_rev"] = v}
class Impl3 : PouchDoc3 {
init {
id = "id1"
rev = "rev1"
}
}
fun keys(a: Any) = js("Object").getOwnPropertyNames(a)
fun printKeys(a: Any) {
println(a::class.simpleName)
println(" instance keys: " + keys(a).toString())
println("__proto__ keys: " + keys(a.asDynamic().__proto__).toString())
println()
}
fun main(args: Array<String>) {
printKeys(Impl1())
printKeys(Impl2())
printKeys(Impl3())
}

I got a good answer from one of the JetBrains guys, Alexey Andreev, over on the JetBrains forum at https://discuss.kotlinlang.org/t/controlling-the-jsname-of-fields-for-pouchdb-interop/2531/. Before I describe that, I'll mention a further failed attempt at refining #bashor's answer.
Property delegates
I thought that #bashor's answer was crying out to use property delegates but I couldn't get that to work without infinite recursion.
class JSMapDelegate<T>(
val jsobject: dynamic
) {
operator fun getValue(thisRef: Any?, property: KProperty<*>): T {
return jsobject[property.name]
}
operator fun setValue(thisRef: Any?, property: KProperty<*>, value: T) {
jsobject[property.name] = value
}
}
external interface PouchDoc4 {
var _id: String
var _rev: String
}
class Impl4() : PouchDoc4 {
override var _id: String by JSMapDelegate<String>(this)
override var _rev: String by JSMapDelegate<String>(this)
constructor(_id: String) : this() {
this._id = _id
}
}
The call within the delegate to jsobject[property.name] = value calls the set function for the property, which calls the delegate again ...
(Also, it turns out you can't put a delegate on a property in an interface, even though you can define a getter/setter pair which work just like a delegate, as #bashor's PouchDoc2 example shows.)
Using an external class
Alexey's answer on the Kotlin forums basically says, "You're mixing the business (with behaviour) and persistence (data only) layers: the right answer would be to explicitly serialise to/from JS but we don't provide that yet; as a workaround, use an external class." The point, I think, is that external classes don't turn into JavaScript which defines property getters/setters, because Kotlin doesn't let you define behaviour for external classes. Given that steer, I got the following to work, which does what I want.
external interface PouchDoc5 {
var _id: String
var _rev: String
}
external class Impl5 : PouchDoc5 {
override var _id: String
override var _rev: String
}
fun <T> create(): T = js("{ return {}; }")
fun Impl5(_id: String): Impl5 {
return create<Impl5>().apply {
this._id = _id
}
}
The output of keys for this is
null
instance keys: _id
__proto__ keys: toSource,toString,toLocaleString,valueOf,watch,unwatch,hasOwnProperty,isPrototypeOf,propertyIsEnumerable,__defineGetter__,__defineSetter__,__lookupGetter__,__lookupSetter__,__proto__,constructor
Creating external classes
Three notes about creating instances of external classes. First, Alexey said to write
fun <T> create(): T = js("{}")
but for me (with Kotlin 1.1) that turns into
function jsobject() {
}
whose return value is undefined. I think this might be a bug, because the official doc recommends the shorter form, too.
Second, you can't do this
fun Impl5(_id: String): Impl5 {
return (js("{}") as Impl5).apply {
this._id = _id
}
}
because that explicitly inserts a type-check for Impl5, which throws ReferenceError: Impl5 is not defined (in Firefox, at least). The generic function approach skips the type-check. I'm guessing that's not a bug, since Alexey recommended it, but it seems odd, so I'll ask him.
Lastly, you can mark create as inline, though you'll need to suppress a warning :-)

Related

Implementing observable properties that can also serialize in Kotlin

I'm trying to build a class where certain values are Observable but also Serializable.
This obviously works and the serialization works, but it's very boilerplate-heavy having to add a setter for every single field and manually having to call change(...) inside each setter:
interface Observable {
fun change(message: String) {
println("changing $message")
}
}
#Serializable
class BlahVO : Observable {
var value2: String = ""
set(value) {
field = value
change("value2")
}
fun toJson(): String {
return Json.encodeToString(serializer(), this)
}
}
println(BlahVO().apply { value2 = "test2" })
correctly outputs
changing value2
{"value2":"test2"}
I've tried introducing Delegates:
interface Observable {
fun change(message: String) {
println("changing $message")
}
#Suppress("ClassName")
class default<T>(defaultValue: T) {
private var value: T = defaultValue
operator fun getValue(observable: Observable, property: KProperty<*>): T {
return value
}
operator fun setValue(observable: Observable, property: KProperty<*>, value: T) {
this.value = value
observable.change(property.name)
}
}
}
#Serializable
class BlahVO : Observable {
var value1: String by Observable.default("value1")
fun toJson(): String {
return Json.encodeToString(serializer(), this)
}
}
println(BlahVO().apply { value1 = "test1" }) correctly triggers change detection, but it doesn't serialize:
changing value1
{}
If I go from Observable to ReadWriteProperty,
interface Observable {
fun change(message: String) {
println("changing $message")
}
fun <T> look(defaultValue: T): ReadWriteProperty<Observable, T> {
return OP(defaultValue, this)
}
class OP<T>(defaultValue: T, val observable: Observable) : ObservableProperty<T>(defaultValue) {
override fun setValue(thisRef: Any?, property: KProperty<*>, value: T) {
super.setValue(thisRef, property, value)
observable.change("blah!")
}
}
}
#Serializable
class BlahVO : Observable {
var value3: String by this.look("value3")
fun toJson(): String {
return Json.encodeToString(serializer(), this)
}
}
the result is the same:
changing blah!
{}
Similarly for Delegates.vetoable
var value4: String by Delegates.vetoable("value4", {
property: KProperty<*>, oldstring: String, newString: String ->
this.change(property.name)
true
})
outputs:
changing value4
{}
Delegates just doesn't seem to work with Kotlin Serialization
What other options are there to observe a property's changes without breaking its serialization that will also work on other platforms (KotlinJS, KotlinJVM, Android, ...)?
Serialization and Deserialization of Kotlin Delegates is not supported by kotlinx.serialization as of now.
There is an open issue #1578 on GitHub regarding this feature.
According to the issue you can create an intermediate data-transfer object, which gets serialized instead of the original object. Also you could write a custom serializer to support the serialization of Kotlin Delegates, which seems to be even more boilerplate, then writing custom getters and setters, as proposed in the question.
Data Transfer Object
By mapping your original object to a simple data transfer object without delegates, you can utilize the default serialization mechanisms.
This also has the nice side effect to cleanse your data model classes from framework specific annotations, such as #Serializable.
class DataModel {
var observedProperty: String by Delegates.observable("initial") { property, before, after ->
println("""Hey, I changed "${property.name}" from "$before" to "$after"!""")
}
fun toJson(): String {
return Json.encodeToString(serializer(), this.toDto())
}
}
fun DataModel.toDto() = DataTransferObject(observedProperty)
#Serializable
class DataTransferObject(val observedProperty: String)
fun main() {
val data = DataModel()
println(data.toJson())
data.observedProperty = "changed"
println(data.toJson())
}
This yields the following result:
{"observedProperty":"initial"}
Hey, I changed "observedProperty" from "initial" to "changed"!
{"observedProperty":"changed"}
Custom data type
If changing the data type is an option, you could write a wrapping class which gets (de)serialized transparently. Something along the lines of the following might work.
#Serializable
class ClassWithMonitoredString(val monitoredProperty: MonitoredString) {
fun toJson(): String {
return Json.encodeToString(serializer(), this)
}
}
fun main() {
val monitoredString = obs("obsDefault") { before, after ->
println("""I changed from "$before" to "$after"!""")
}
val data = ClassWithMonitoredString(monitoredString)
println(data.toJson())
data.monitoredProperty.value = "obsChanged"
println(data.toJson())
}
Which yields the following result:
{"monitoredProperty":"obsDefault"}
I changed from "obsDefault" to "obsChanged"!
{"monitoredProperty":"obsChanged"}
You however lose information about which property changed, as you don't have easy access to the field name. Also you have to change your data structures, as mentioned above and might not be desirable or even possible. In addition, this work only for Strings for now, even though one might make it more generic though.
Also, this requires a lot of boilerplate to start with. On the call site however, you just have to wrap the actual value in an call to obs.
I used the following boilerplate to get it to work.
typealias OnChange = (before: String, after: String) -> Unit
#Serializable(with = MonitoredStringSerializer::class)
class MonitoredString(initialValue: String, var onChange: OnChange?) {
var value: String = initialValue
set(value) {
onChange?.invoke(field, value)
field = value
}
}
fun obs(value: String, onChange: OnChange? = null) = MonitoredString(value, onChange)
object MonitoredStringSerializer : KSerializer<MonitoredString> {
override val descriptor: SerialDescriptor = PrimitiveSerialDescriptor("MonitoredString", PrimitiveKind.STRING)
override fun serialize(encoder: Encoder, value: MonitoredString) {
encoder.encodeString(value.value)
}
override fun deserialize(decoder: Decoder): MonitoredString {
return MonitoredString(decoder.decodeString(), null)
}
}

Pass different generic types to function

I have a function in Kotlin that can take a generic object as a parameter. The two objects are unrelated and do not share any base types. They both however implement the same functions. I would like to re-use those functions within my function. Something along these lines:
fun storeUser(datastore: Any) {
datastore.storeName("John")
}
// Call the function
val datastore1 = DataStore1()
storeUser(datastore1)
val datastore2 = DataStore2()
storeUser(datastore2)
Both the DataStore1 and DataStore2 have a function called "storeName". Is there a way in Kotlin to re-use this function in the storeUser function? I tried playing around with Generics but this does not seem possible.
The example code above is simple. In my real app, there are many more functions beside storeName. If I can't have a common function to store my data, I will need to create two separate functions and duplicate the storage for both. That kind of sucks.
I recommend using a common interface for both classes. If they are provided by a thid-party library, you could wrap them in your own classes and interface.
If you don't want to do that, you could just check the type of the parameter in the storeUser function:
fun storeUser(datastore: Any) {
when(datastore) {
is DataStore1 -> datastore.storeName("John")
is DataStore2 -> datastore.storeName("John")
else -> throw IllegalArgumentException()
}
}
But note that if you have another datastore in the future, you will need to add one more is clause to this function. That makes this code not very maintainable...
Better solution
If you create an interface Datastore:
interface Datastore {
fun storeName(name: String)
}
and the make your datastores implement it:
class Datastore1 : Datastore {
//Datastore1.storeName implementation
}
class Datastore2 : Datastore {
//Datastore2.storeName implementation
}
Then, you don't need to check the types in storeUser function. Just change its parameter type to Datastore:
fun storeUser(datastore: Datastore) {
datastore.storeName("John")
}
If Datastore1 and Datastore2 are provided by a third-party library, you can wrap them in your own classes and implement your Datastore interface:
class FirstDatastore : Datastore {
private val datastore = DataStore1()
override fun storeName(name: String) {
datastore.storeName(name)
}
}
class SecondDatastore : Datastore {
private val datastore = DataStore2()
override fun storeName(name: String) {
datastore.storeName(name)
}
}
So you can call your function using your classes:
val datastore1 = FirstDatastore()
storeUser(datastore1)
val datastore2 = SecondDatastore()
storeUser(datastore2)
As I said in the comment to the question, it would really be better to write a common interface for these classes. If that's not possible because the classes come from an external dependency, the second best thing to do would be to wrap the code as Héctor did.
Kotlin is a statically typed language, so unfortunately wrapping code like this results in a lot of duplication. If you didn't want to write a new wrapper for every new instance of the DataStore, you could use reflection to call it dynamically. This way you only have to write the definition once. However, you forego all the compile-time benefits of static checks, so it's not really a good idea. It was good to do as an exercise though. 😎
class WrappedDataStore<T : Any>(private val dataStore: T) {
private fun callDynamically(methodName: String, vararg args: Any?) {
val argTypes = args.map { it?.let { it::class.java} }.toTypedArray()
dataStore.javaClass
.getMethod(methodName, *argTypes)
.invoke(dataStore, *args)
}
fun storeName(name: String) = callDynamically("storeName", name)
}
fun <T : Any> storeUser(dataStore: WrappedDataStore<T>) =
dataStore.storeName("John")
fun main() {
val one = WrappedDataStore(DataStore1())
val two = WrappedDataStore(DataStore2())
one.storeName("foo")
two.storeName("bar")
storeUser(one)
storeUser(two)
}
class DataStore1 {
fun storeName(foo: String) = println("DataStore1 $foo")
}
class DataStore2 {
fun storeName(bar: String) = println("DataStore2 $bar")
}
Output:
DataStore1 foo
DataStore2 bar
DataStore1 John
DataStore2 John

How to use Internationalization in tornadofx

I am trying to use internationalization in a Kotlin application using the tornadofx framework.
I have created a properties file and depending on the selected language the correct file is loaded. But when I want to change the language in the running application the UI does not update accordingly.
For internationalization you should use a companion object to get the related translation anywhere in your application.
First of all your translation class should know which is the actual selected language/locale. For this I use an enum with the possible locales for the application:
fun setLocale(locale: SupportedLocale) {
if (SupportedLocale.supportedLocals.contains(locale)) {
Locale.setDefault(locale.local)
actualLocal = locale.local
//Good practice would be to store it in a properties file to have the information after restart
} else {
//Throw a warning or sth with your preferred logger
}
}
Then we need a method which gets the particular string value from your resource bundle like:
operator fun get(#PropertyKey(resourceBundle = BUNDLE_NAME) key: String, vararg args: Any): String {
val bundle = ResourceBundle.getBundle(BUNDLE_NAME, actualLocal)
return MessageFormat.format(bundle.getString(key), *args)
}
In JavaFx applications (also TornadoFX) you should use StringBindings (https://docs.oracle.com/javase/8/javafx/api/javafx/beans/binding/StringBinding.html) for example to bind a label text property to your translated string. For that we will implement a special method:
fun createStringBinding(#PropertyKey(resourceBundle = BUNDLE_NAME) key: String, vararg args: Any): StringBinding {
return Bindings.createStringBinding(Callable { get(key, *args) }, Settings.languageProperty())
}
Now you can use your object like this:
textProperty().bind(MyLang.createStringBinding("MyApp.MyTranslation"))
Here an runnable example:
MyLang.kt
enum class SupportedLocale(val local:Locale) {
ENGLISH(Locale.ENGLISH),
GERMAN(Locale.GERMAN);
companion object {
val supportedLocals: List<SupportedLocale>
get() = SupportedLocale.values().toList()
}
}
class MyLang {
companion object {
private const val BUNDLE_NAME = "Language" //prefix of your resource bundle
private var actualLocal = Locale.getDefault()
fun setLocale(locale: SupportedLocale) {
if (SupportedLocale.supportedLocals.contains(locale)) {
Locale.setDefault(locale.local)
actualLocal = locale.local
//Good practice would be to store it in a properties file to have the information after restart
} else {
//Throw a warning or sth with your preferred logger
}
}
operator fun get(#PropertyKey(resourceBundle = BUNDLE_NAME) key: String, vararg args: Any): String {
val bundle = ResourceBundle.getBundle(BUNDLE_NAME, actualLocal)
return MessageFormat.format(bundle.getString(key), *args)
}
fun createStringBinding(#PropertyKey(resourceBundle = BUNDLE_NAME) key: String, vararg args: Any): StringBinding {
return Bindings.createStringBinding(Callable { get(key, *args) }, Settings.languageProperty())
}
}
}
fun main() {
println("My translation: " + MyLang.createStringBinding("MyApp.MyTranslation").get())
//The get() here is only to get the string for assign a property its not needed like in the example
}
If you need any explanations or its unclear. Just ask! Its just written down maybe I forgot something to explain.

How to make field required in kotlin DSL builders

In Kotlin, when creating a custom DSL, what is the best way to force filling required fields inside the builder's extension functions in compile time. E.g.:
person {
name = "John Doe" // this field needs to be set always, or compile error
age = 25
}
One way to force it is to set value in a function parameter instead of the body of the extension function.
person(name = "John Doe") {
age = 25
}
but that makes it a bit more unreadable if there are more required fields.
Is there any other way?
New type inference enables you to make a null-safe compile-time checked builder:
data class Person(val name: String, val age: Int?)
// Create a sealed builder class with all the properties that have default values
sealed class PersonBuilder {
var age: Int? = null // `null` can be a default value if the corresponding property of the data class is nullable
// For each property without default value create an interface with this property
interface Named {
var name: String
}
// Create a single private subclass of the sealed class
// Make this subclass implement all the interfaces corresponding to required properties
private class Impl : PersonBuilder(), Named {
override lateinit var name: String // implement required properties with `lateinit` keyword
}
companion object {
// Create a companion object function that returns new instance of the builder
operator fun invoke(): PersonBuilder = Impl()
}
}
// For each required property create an extension setter
fun PersonBuilder.name(name: String) {
contract {
// In the setter contract specify that after setter invocation the builder can be smart-casted to the corresponding interface type
returns() implies (this#name is PersonBuilder.Named)
}
// To set the property, you need to cast the builder to the type of the interface corresponding to the property
// The cast is safe since the only subclass of `sealed class PersonBuilder` implements all such interfaces
(this as PersonBuilder.Named).name = name
}
// Create an extension build function that can only be called on builders that can be smart-casted to all the interfaces corresponding to required properties
// If you forget to put any of these interface into where-clause compiler won't allow you to use corresponding property in the function body
fun <S> S.build(): Person where S : PersonBuilder, S : PersonBuilder.Named = Person(name, age)
Use case:
val builder = PersonBuilder() // creation of the builder via `invoke` operator looks like constructor call
builder.age = 25
// builder.build() // doesn't compile because of the receiver type mismatch (builder can't be smart-casted to `PersonBuilder.Named`)
builder.name("John Doe")
val john = builder.build() // compiles (builder is smart-casted to `PersonBuilder & PersonBuilder.Named`)
Now you can add a DSL function:
// Caller must call build() on the last line of the lambda
fun person(init: PersonBuilder.() -> Person) = PersonBuilder().init()
DSL use case:
person {
name("John Doe") // will not compile without this line
age = 25
build()
}
Finally, on JetBrains open day 2019 it was said that the Kotlin team researched contracts and tried to implement contracts that will allow creating safe DSL with required fields. Here is a talk recording in Russian. This feature isn't even an experimental one, so
maybe it will never be added to the language.
In case you're developing for Android I wrote a lightweight linter to verify mandatory DSL attributes.
To solve your use case you will only need to add an annotation #DSLMandatory to your name property setter and the linter will catch any place when it is not assigned and display an error:
#set:DSLMandatory
var name: String
You can take a look here:
https://github.com/hananrh/dslint/
Simple, throw an exception if it's not defined in your DLS after the block
fun person(block: (Person) -> Unit): Person {
val p = Person()
block(p)
if (p.name == null) {
// throw some exception
}
return p
}
Or if you want to enforce it at build time, just make it return something useless to the outer block if not defined, like null.
fun person(block: (Person) -> Unit): Person? {
val p = Person()
block(p)
if (p.name == null) {
return null
}
return p
}
I'm guessing your going off this example so maybe address would be the better example case:
fun Person.address(block: Address.() -> Unit) {
// city is required
var tempAddress = Address().apply(block)
if (tempAddress.city == null) {
// throw here
}
}
But what if we wanted to ensure everything was defined, but also wanted to let you do it in any order and break at compile time. Simple, have two types!
data class Person(var name: String = null,
var age: Int = null,
var address: Address = null)
data class PersonBuilder(var name: String? = null,
var age: Int? = null,
var address: Address? = null)
fun person(block: (PersonBuilder) -> Unit): Person {
val pb = PersonBuilder()
block(p)
val p = Person(pb.name, pb.age, pb.address)
return p
}
This way, you get to you the non-strict type to build, but it better be null-less by the end. This was a fun question, thanks.

Kotlin - extensible type-safe builders

I want to be able to create a custom builder-pattern DSL-type thing, and I want the ability to create new components in a clean and type-safe way. How can I hide the implementation details required for creating and extending such a builder-pattern?
The Kotlin docs give something like the following example:
html {
head {
title {+"XML encoding with Kotlin"}
}
body {
h1 {+"XML encoding with Kotlin"}
p {+"this format can be used as an alternative markup to XML"}
a(href = "http://kotlinlang.org") {+"Kotlin"}
// etc...
}
}
Here, all of the possible "elements" are predefined and implemented as functions that also return objects of the corresponding types. (eg. the html function returns an instance of the HTML class)
Each function is defined so that it adds itself to its parent context's object as a child.
Suppose someone wanted to create a new element type NewElem usable as newelem. They would have to do something cumbersome such as:
class NewElem : Element() {
// ...
}
fun Element.newelem(fn: NewElem.() -> Unit = {}): NewElem {
val e = NewElem()
e.fn()
this.addChild(e)
return e
}
every time.
Is there a clean way to hide this implementation detail?
I want to be able to create a new element by simply extending Element for example.
I do not want to use reflection if possible.
Possibilities I Tried
My main problem is coming up with a clean solution. I thought of a couple other approaches that didn't pan out.
1) Create new elements with a function call that returns a function to be used in the builder style such as:
// Pre-defined
fun createElement(...): (Element.() -> Unit) -> Element
// Created as
val newelem = createElement(...)
// Used as
body {
newelem {
p { +"newelem example" }
}
}
There are obvious downsides to this, and I don't see a clear way to implement it either - probably would involve reflection.
2) Override the invoke operator in companion object
abstract class Element {
companion object {
fun operator invoke(build: Element.() -> Unit): Element {
val e = create()
e.build()
return e
}
abstract fun create(): Element
}
}
// And then you could do
class NewElem : Element() {
companion object {
override fun create(): Element {
return NewElem()
}
}
}
Body {
NewElem {
P { text = "NewElem example" }
}
}
It is unfortunately not possible to enforce "static" functions to be implemented by subclasses in a type-safe way.
Also, companion objects aren't inherited, so the invoke on subclasses wouldn't work anyway.
And we again run into problems about adding children elements to the correct context, so the builder doesn't actually build anything.
3) Override the invoke operator on Element types
abstract class Element {
operator fun invoke(build: Element.() -> Unit): Element {
this.build()
return this
}
}
class NewElem(val color: Int = 0) : Element()
Body() {
NewElem(color = 0xff0000) {
P("NewElem example")
}
}
This might have worked, except for when you immediately try to invoke on the object created by the constructor call, the compiler cannot tell that the lambda is for the "invoke" call and tries to pass it into the constructor.
This can be fixed by making something slightly less clean:
operator fun Element.minus(build: Element.() -> Unit): Element {
this.build()
return this
}
Body() - {
NewElem(color = 0xff0000) - {
P("NewElem example")
}
}
But yet again, adding children elements to the parent elements isn't actually possible without reflection or something similar, so the builder still doesn't actually build anything.
4) Calling add() for sub-elements
To try to fix the issue of the builder not actually building anything, we could implement an add() function for sub-elements.
abstract class Element {
fun add(elem: Element) {
this.children.add(elem)
}
}
Body() - {
add(NewElem(color = 0xff0000) - {
add(P("NewElem red example"))
add(P("NewElem red example 2"))
})
add(NewElem(color = 0x0000ff) - {
add(P("NewElem blue example"))
})
}
But this is obviously not clean and is just deferring the cumbersome-ness to the usage side instead of the implementation side.
I think it's unavoidable to add some sort of a helper function for each Element subclass you create, but their implementation can be simplified with generic helper functions.
For example, you can create a function that performs the setup call and adds the new element to the parent, then you only have to call into this function and create an instance of your new element:
fun <T : Element> Element.nest(elem: T, fn: T.() -> Unit): T {
elem.fn()
this.addChild(elem)
return elem
}
fun Element.newElem(fn: NewElem.() -> Unit = {}): NewElem = nest(NewElem(), fn)
Alternatively, you could create that instance via reflection to simplify even further, but since you've stated you'd like to avoid it, this will probably seem unnecessary:
inline fun <reified T : Element> Element.createAndNest(fn: T.() -> Unit): T {
val elem = T::class.constructors.first().call()
elem.fn()
this.addChild(elem)
return elem
}
fun Element.newElem(fn: NewElem.() -> Unit = {}) = createAndNest(fn)
These still leave you with having to declare a factory function with the appropriate header, but this is the only way to achieve the syntax that the HTML example achieves, where a NewElem can be created with its own newElem function.
I came up with a solution that isn't the most elegant, but it is passable and works the way I would want it to.
It turns out that if you override an operator (or create any extension function for that matter) inside a class, it has access to its parent context.
So I overrode the unary + operator
abstract class Element {
val children: ArrayList<Element> = ArrayList()
// Create lambda to add children
operator fun minus(build: ElementCollector.() -> Unit): Element {
val collector = ElementCollector()
collector.build()
children.addAll(collector.children)
return this
}
}
class ElementCollector {
val children: ArrayList<Element> = ArrayList()
// Add child with unary + prefix
operator fun Element.unaryPlus(): Element {
this#ElementCollector.children.add(this)
return this
}
}
// For consistency
operator fun Element.unaryPlus() = this
This allows me to create new elements and use them like this:
class Body : Element()
class NewElem : Element()
class Text(val t: String) : Element()
fun test() =
+Body() - {
+NewElem()
+NewElem() - {
+Text("text")
+Text("elements test")
+NewElem() - {
+Text("child of child of child")
}
+Text("it works!")
}
+NewElem()
}