EDIT: I think this code might help to clarify what I’m talking about. In the original example, html
is a Kotlin DSL, defined as you normally would:
inline fun <T> html(block: HtmlBuilder.() -> T): T {
contract { callsInPlace(block, InvocationKind.EXACTLY_ONCE) }
return HtmlBuilder().block()
}
@DslMarker
annotation class HtmlDsl
@HtmlDsl
class HtmlBuilder {
fun header(): HtmlHeader = ...
fun body(): HtmlBody = ...
fun paragraph(text: String): HtmlParagraph = ...
}
I just want the ability to invoke the “DSL” functions of HtmlBuilder
in property initializers of any given class. The only way to currently achieve this is via an anonymous object inside the builder (see above.) You cannot currently achieve this in a named class, the alternatives with identical effect are either too verbose or brittle imo.
Hopefully that helps, but if still not clear, read on.
How would the compiler know whether val bar =
should create a local variable or a property?
The same way it currently does. You cannot define local variables inside a class, only properties.
What about functions and other features of the class?
Functions work the same as before - their scope is unaffected. Only initializers and possibly constructors would have access to DSL functions/methods. Companion objects are also unaffected. Not sure what other features there are to consider.
what it does basically is to help defining multiple similar classes, according to some pattern.
Similar how? Classes can have whatever properties you create for them. They can be arbitrarily different, as can a normal class. The only difference here is that the initializers and possibly constructors have access to DSL methods. I don’t see how OOP is related here.
Regarding OOP, I suppose you could define your DSL as an abstract class that your “container” classes could inherit from. But I feel this is lacking for a few reasons -
- You can only inherit from one abstract class, meaning its a highly contended resource. Also, while you could use protected methods in this case, you couldn’t reuse the “DSL” outside of this context.
- Interface default methods otoh are always public facing in Kotlin, and other JVM languages I’m aware of. Presumably you wouldn’t want clients of the class to see the DSL methods used to instantiate it.
Ultimately it feels like the wrong tool for the job - if one of the two points were to change, I’d be more amenable to it, however.
What is your specific use case?
I provided one with html elements that I think would apply to a lot of people, but I have several that I actively use - mainly UI definition, and various OpenGL constructs - render pipeline, vertex layouts, shader compilation into programs, etc.
Here’s an example class with some ugliness that could be fixed by this feature:
class Programs(
device: GLDevice,
private val paths: NebRaxPaths,
) {
val manager = ProgramManager(device, paths.shaderLogsDir, ::getShaderReader)
private val programList = ArrayList<Program>(64)
private fun getShaderReader(resourceName: String): Reader {
return paths.shaderDirs.firstNotNullOfOrNull { it.resolve(resourceName).takeIf { it.isRegularFile() } }?.reader()
?: error("Couldn't find shader resource with name '$resourceName'!")
}
// I'd like to eliminate the following "DSL redefinition":
private fun program(vararg shaders: Shader) = manager.makeProgram(*shaders).also { programList += it.acquire() }
private fun program(vararg shaders: String) = manager.makeProgram(*shaders).also { programList += it.acquire() }
private fun shader(shaderKey: String) = manager.makeShader(shaderKey)
private fun shader(shaderKey: String, constParams: Map<String, String>) = manager.makeShader(shaderKey, constParams)
private fun shader(shaderKey: String, vararg constParams: String) = manager.makeShader(shaderKey, *constParams)
private fun constParams(vararg keyValues: String) = ProgramManager.makeConstParams(*keyValues)
//////////////////////////////////////////////////////////////////////////////////////////////////////////////////////
// Generic
val simpleTextured = program("simple/textured.glvs", "simple/textured.glfs")
val simpleDepth = program("simple/textureToDepth.glvs", "simple/textureToDepth.glfs")
val textureToDepth = program("util/texture_to_depth.glvs", "util/texture_to_depth.glfs")
val color = program("simple/color.glvs", "simple/color.glfs")
val colorTextured = program("simple/color_textured.glvs", "simple/color_textured.glfs")
val colorAlphaTextured = program("simple/color_textured.glvs", "simple/color_alpha_textured.glfs")
val colorLayerTextured = program("simple/color_layer_textured.glvs", "simple/color_layer_textured.glfs")
val colorTextureMasked = program("simple/color_textured.glvs", "simple/color_texture_masked.glfs")
val colorIntTextured = program("simple/color_textured.glvs", "simple/color_int_textured.glfs")
val colorDepthTextured = program("simple/color_textured.glvs", "simple/color_depth_textured.glfs")
val blockDetailed = program("block/detailed.glvs", "block/block.glgs", "gbuffer/write_tex2D.glfs")
val blockTiny = program("block/tiny.glvs", "block/block.glgs", "gbuffer/write_tex2D.glfs")
val blockMarchinglyCubed = program("block/marchingly_cubed.glvs", "block/marchingly_cubed.glgs", "gbuffer/write_multi_mat.glfs")
val blockMarchinglyCubedIT = program("block/marchingly_cubed_instanced_triangles.glvs", "gbuffer/write_multi_mat.glfs")
val blockMarchinglyCubedFeedback = program(
"block/marchingly_cubed.glvs",
"block/marchingly_cubed_feedback.glgs"
).setTransformFeedbackVaryings(*MarchingCubesCompressedAttributeStrategy.BLOCK_MESH_TRANSFORM_FEEDBACK_VARYINGS)
val blockMarchinglyCubedDepth = program("block/marchingly_cubed.glvs", "block/marchingly_cubed.glgs", "simple/depth.glfs")
val light = program(
shader("lighting/light.glvs"),
shader("lighting/light.glfs", constParams("DISABLE_MATERIAL_UNIFORM", Config.DISABLE_MATERIAL_UNIFORM.toString())))
val smaaEdge = program("anti_aliasing/smaa_edge.glvs", "anti_aliasing/smaa_edge.glfs")
val smaaWeight = program("anti_aliasing/smaa_weight.glvs", "anti_aliasing/smaa_weight.glfs")
val smaaBlend = program("anti_aliasing/smaa_blend.glvs", "anti_aliasing/smaa_blend.glfs")
val blackHole = program("blackhole.glvs", "blackhole.glfs")
val nanInfPass = program("simple/color_textured.glvs", "util/nan_inf_pass.glfs")
//////////////////////////////////////////////////////////////////////////////////////////////////////////////////////
init {
manager.compilePending()
device.exitOnGLError("programs")
}
fun onApplicationClosed() {
programList.forEachNoGC { it.release() }
programList.clear()
}
}
Having to redefine methods like that for every application is brittle and far less than ideal, but the alternative is predeclaring every single program variable before assigning to it in an initializer block. This is a fortunate case in that the DSL is simple enough to be redefined in five lines, but for many DSLs this would not be a reasonable solution. Here’s a UI definition demonstrating how bad this can be:
class GuiHud(
builder: GuiBuilder,
private val pivotProvider: PRenderPivotProvider
) : PanelComponent(), IInGameWindow, IHudIndicatorProvider {
// I'd like to eliminate all of these variable "pre-declarations":
private val lblPlayerHeartRate: LabelComponent
private val lblPlayerRadioactives: LabelComponent
private val lblPlayerSelectedBlockType: LabelComponent
private val lblPlayerShapePlaceMode: LabelComponent
private val lblPlayerShapePlaceType: LabelComponent
private val lblPlayerToolSnapping: LabelComponent
private val mtrPlayerStamina: GuiMeter
private val mtrPlayerBlood: GuiMeter
private val mtrMultiRadioactives: GuiMeterMulti
private val lblMessageCentered: LabelComponent
private val lblMessageBottom: LabelComponent
init { builder.applyAndBuild(this) {
dock = FILL
backgroundColor = 0
panel {
dock = LEFT
preferredWidth = 300
backgroundColor = 0
panel {
dock = TOP
setMargin(10, 0, 10, 0)
backgroundColor = 0
lblPlayerHeartRate = label("BPM: 60") {
dock = Dock.RIGHT
marginX0 = 2
preferredWidth = 70
contentAlignment = CENTER
backgroundColor = 0x000000ff
}
mtrPlayerStamina = add(GuiMeter()) {
dock = TOP
preferredHeight = 16
foregroundColor = STAMINA_COLOR_RGBA
backgroundColor = STAMINA_BG_COLOR_RGBA
}
mtrPlayerBlood = add(GuiMeter()) {
dock = TOP
preferredHeight = 16
marginY0 = 2
value1Color = BLOOD_LOSS_COLOR_RGBA
foregroundColor = BLOOD_COLOR_RGBA
backgroundColor = 0x000000ff
}
}
}
val pnlRadioactivesWidth = 24
panelFlowUp {
dock = Dock.RIGHT
preferredWidth = pnlRadioactivesWidth
backgroundColor = 0
lblPlayerRadioactives = label("???") {
preferredHeight = 20
contentAlignment = CENTER
foregroundColor = -1
backgroundColor = 0
}
mtrMultiRadioactives = add(GuiMeterMulti()) {
setMargin(2)
isFilledReverse = true
isVertical = true
overflowBehavior = FIT
foregroundColor = -1
backgroundColor = 0x000000ff
}
}
panelFlowDown {
setAnchorSrcDst(BOTTOM_RIGHT, BOTTOM_RIGHT)
preferredWidth = 400
backgroundColor = 0
childConfig { preferredHeight = 20; backgroundColor = 0; isHidden = true }
lblPlayerShapePlaceType = label("Placement Shape: Sphere") { contentAlignment = RIGHT }
lblPlayerShapePlaceMode = label("Placement Mode: Normal") { contentAlignment = RIGHT }
lblPlayerSelectedBlockType = label("Selected Material: None") { contentAlignment = RIGHT }
lblPlayerToolSnapping = label("Voxel Snapping: Off") { contentAlignment = RIGHT }
}
withChildConfig({
marginX1 = 300 - pnlRadioactivesWidth
backgroundColor = 0
isHidden = true
}) {
lblMessageBottom = label("") {
dock = BOTTOM
preferredHeight = 20
contentAlignment = CENTER
}
lblMessageCentered = label("") {
dock = FILL
marginY0 = 20
contentAlignment = CENTER
}
}
}}
// Insert class behavior here....
}
It’s just a lot of boilerplate. Not as many lines as the Programs
class would require, but still not great. Having to look in two places to modify one thing is also not ideal. I imagine a lot of UI or client-side web code would benefit from this feature, given the prevalence of DSLs there in defining basic components.