I try to describe Kotlin syntax itself with a DSL, and I struggle a bit with the more “loose” or “independent” elements, like annotations or modifiers. Take this piece of code:
Behind the scenes it works by adding PRIVATE and CONST to a mutable list in the scope, and then the VAL function collects its modifiers from this list and clears it. I will implement a similar solution for annotations.
In the example I’m less concerned with the VAL part, I can certainly improve it, but I don’t like the modifiers, e.g. the need for semicolons. Something like +PRIVATE; +CONST; ... would save a character, but I’m not sure if I like it.
I would love something like @PRIVATE @CONST ..., but there seems no way to get this syntax to work, at least not without annotation processors or compiler plugins.
Did I miss a trick, or is there really no more wiggle room regarding the syntax?
This is still a valid question. I think there are many more things to consider (like getters and setters), so VAL may end up pretty crowded. Therefore, getting modifiers (and annotations) out of the way seems to be the better approach. In my opinion, having to remember where to put the modifiers in a long argument list feels a bit like defeating the purpose of a DSL.
And of course, this is just an example. The modifier problem occurs basically everywhere, and I’m not sure if I should “waste” the only available (*) vararg parameter for this.
*) I know I can have multiple varargs in an argument list, but then I cannot use them without either named access or spread operator, so in the context of a DSL there is just one “good” vararg.
Hmmm, if you make your annotations have BINARY retention, then I think you could maybe do something with delegated properties?
@PRIVATE
@CONST
val foo: String by magicDslFunction("bar")
where magicDslFunction is reified, and returns something with a provideDelegate that reads the name and annotations of the property (I think you can use reflection to access the annotations on a local delegated property but I’m not 100% certain)
magicDslFunction and provideDelegate are your friends here. They’ll let you do exactly any dynamic code creation that you want. Think of the provided example as translating to:
val kprop = SomeIntrinsicKPropertyConstructor(name = "foo", annotations = listOf(PRIVATE, CONST), type = String::class)
magicDslFunction<String>("bar").provideDelegate(null, kprop)
and so inside of provideDelegate you can access all the necessary info.
If you have a repo with your WIP code, I could make a little example of how to implement this, if you’d like!
This syntax can also be tweaked a bit so that you can refer to foo for instance later on in the generated code, and get type checking for free!
Something like this should work (playground doesn’t have jvm reflection, so the annotation reading doesn’t work. Should work fine as long as you add that dependency though):
import kotlin.reflect.*
fun main() {
println(build {
@PRIVATE
@CONST
val foo by VAL("bar")
})
}
annotation class PRIVATE
annotation class CONST
enum class Visibility(val display: String) {
Private("private"),
Internal("internal"),
;
override fun toString() = display
}
sealed interface Expression<T>
operator fun <E: Expression<T>, T> E.getValue(thisRef: Any?, prop: KProperty<*>): E = this
class Const<T>(val value: T): Expression<T> {
override fun toString() = if(value is String) """"$value"""" else "$value"
}
class Local<T>(val name: String): Expression<T> {
override fun toString() = name
}
class KotlinBuilder {
internal val output = StringBuilder()
fun <T> VAL(initial: T): LocalDelegate<T> = VAL(Const(initial))
fun <T> VAL(initial: Expression<T>): LocalDelegate<T> = LocalDelegate(initial)
fun <T> add(local: Local<T>, type: KType, value: Expression<T>, visibility: Visibility = Visibility.Internal, isConst: Boolean = false) {
output.append("$visibility ${if(isConst) "const " else ""}val ${local.name}: ${(type.classifier!! as KClass<*>).simpleName} = $value")
}
inline operator fun <reified T> LocalDelegate<T>.provideDelegate(thisRef: Any?, prop: KProperty<*>): Local<T> = Local<T>(prop.name).also{
val visibility = if(prop.annotations.any { it is PRIVATE }) Visibility.Private else Visibility.Internal
val isConst = prop.annotations.any { it is CONST }
add(it, typeOf<T>(), expr, visibility, isConst)
}
}
class LocalDelegate<T>(val expr: Expression<T>)
fun build(block: KotlinBuilder.() -> Unit) = KotlinBuilder().apply(block).output.toString()
I made a lot of assumptions and simplifications in designing this, but the basic idea should be easy to see
How about in addition to the objects for each modifier defined the infix function, which takes KModifier as its arguments and returns the KModifier, which represents the combination of its arguments.
This way you can chain modifiers without parentheses and semicolons.
I think I’ll go with an operator. It’s not pretty, especially because .. evaluates from left to right. Here is a stripped down example, in case anyone needs something similar:
enum class Mod {
PUBLIC,
PRIVATE,
ABSTRACT
}
data class Value(val name: String, val mods: MutableList<Mod> = mutableListOf<Mod>())
object Terminal
class Scope {
val values = mutableListOf<Value>()
fun VAL(name: String) = Terminal.also {
values += Value(name)
}
operator fun Mod.rangeTo(mc: Terminal) {
values.last().mods += this
}
operator fun List<Mod>.rangeTo(mc: Terminal) {
values.last().mods += this
}
operator fun Mod.rangeTo(mod: Mod) = listOf(this, mod)
operator fun List<Mod>.rangeTo(mod: Mod) = this + mod
}
fun scope(lambda: Scope.() -> Unit): List<Value> =
Scope().apply(lambda).values
fun main() {
val list = scope {
VAL("a")
PRIVATE .. VAL("b")
PRIVATE .. PUBLIC .. VAL("c")
ABSTRACT .. PRIVATE .. PUBLIC .. VAL("d")
}
println(list)
}