danilo.pianini@unibo.itCompiled on: 2025-11-21 — printable version
Languages that capture a specific domain
Being able to express a DSL requires a formalization of the domain
which in turn requires a deep understanding of the domain
Languages with a flexible syntax are good candidates to host DSLs:
implicits, infixing, currying, mixed brackets)Key features for building DSLs:
infix callsDomain-specific languages require, as first step, to have a domain model.
Once the domain entities are available,
they will be elegantly instanced via DSL.
The business logic will then be bound to the domain model inextricably.
The domain is better modelled with interfaces,
whose implementations are manipulated by an infrastructure exposing the DSL
Desired syntax:
html {
head {
title { -"A link to the unibo webpage" }
}
body {
p("class" to "myCustomCssClass") {
a(href = "http://www.unibo.it") { -"Unibo Website" }
}
}
}.render()
Result:
<html>
<head>
<title>
A link to the unibo webpage
</title>
</head>
<body>
<p class="myCustomCssClass">
<a href="http://www.unibo.it">
Unibo Website
</a>
</p>
</body>
</html>
ElementsElements can be either Tags or plain TextElements can be RepeatableText elements are always RepeatableTags can be Repeatableinterface Element
interface RepeatableElement : Element
interface Tag : Element
interface RepeatableTag : Tag, RepeatableElement
interface TextElement : RepeatableElement
Elements can be rendered to plain text, possibly with some indentationinterface Element {
fun render(indent: String = ""): String
}
TextElements are composed of simple text, possibly rendered with intentationinterface TextElement : RepeatableElement {
val text: String
override fun render(indent: String) = "$indent$text\n"
}
name, they can have children Elements, and key/value attributesinterface Tag : Element {
val name: String
val children: List<Element>
val attributes: Map<String, String>
}
String as indentationPairs is not very clean:
we convey a better meaning to what we are doing by relying on a typealias AttributeTextElement is trivial to implement, it’s just textconst val INDENT = "\t" // Compile time constant
typealias Attribute = Pair<String, String> // Just to avoid writing Pair<String, String>
data class Text(override val text: String) : TextElement
TagTag is a bit more complex, let’s try to factor the common part of all Tags into an AbtractTag
name is easyAttributes get registered at creation time. We use a vararg to make the construction nicerchildrenregister sub elements
Repeatable or notabstract class AbstractTag(override val name: String, vararg attributes: Attribute) : Tag {
final override var children: List<Element> = emptyList() // Override val with var
private set(value) { field = value } // Write access only from this class
final override val attributes: Map<String, String> = attributes.associate { it }
fun registerElement(element: Element) {
if (element is RepeatableElement || children.none { it::class == element::class }) {
children = children + element
} else {
error("cannot repeat tag ${element::class.simpleName} multiple times:\n$element")
}
}
}
Tag’s renderingTag is renderedabstract class AbstractTag(override val name: String, vararg attributes: Attribute) : Tag {
...
final override fun render(indent: String) = // Rendering by multiline string!
"""
|$indent<$name${renderAttributes()}>
|${renderChildren(indent + INDENT)}
|$indent</$name>
"""
.trimMargin() // Trims what's left of a |
.replace("""\R+""".toRegex(), "\n") // In case there are no children, no empty lines
private fun renderChildren(indent: String): String =
children.map { it.render(indent) }.joinToString(separator = "\n")
private fun renderAttributes(): String = attributes.takeIf { it.isNotEmpty() }
?.map { (attribute, value) -> "$attribute=\"$value\"" } // Safe fluent calls
?.joinToString(separator = " ", prefix = " ")
?: "" // Elvis operator
}
We will now:
Note: In this example we mix the language and model implementation.
It’d be cleaner to define a complete API (favoring immutability),
and then wrap it in a DSL (where mutability is mandatory)
class HTML(vararg attributes: Attribute = arrayOf()) : AbstractTag("html", *attributes)
fun html(vararg attributes: Attribute, init: HTML.() -> Unit): HTML = HTML(*attributes).apply(init)
vararg they are implicitly optional)class representing our entry pointWe can now write:
html { }.render()
html("lang" to "en") { }.render()
producing:
<html>
</html>
<html lang="en">
</html>
class Head : AbstractTag("head") {
fun title(configuration: Title.() -> Unit = { }) = registerElement(Title().apply(configuration))
}
abstract class TagWithText(name: String, vararg attributes: Attribute) : AbstractTag(name, *attributes) {
// Scoping via member extensions!
operator fun String.unaryMinus() = registerElement(Text(this)) // Syntax for writing plain text
}
class Title : TagWithText("title")
class Body(vararg attributes: Attribute) : TagWithText("body", *attributes)
const val newline = "<br/>"
html("lang" to "en") {
head { title { -"An experiment" } }
body {
-"My contents"
-newline
-"And some more contents"
}
}
abstract class BodyTag(name: String, vararg attributes: Attribute) : TagWithText(name, *attributes) {
// <a> and <p> can be nested everywhere in the body
fun p(vararg attributes: Attribute, configuration: P.() -> Unit) =
registerElement(P(*attributes).apply(configuration))
fun a(href: String? = null, vararg attributes: Attribute, configuration: Anchor.() -> Unit) =
registerElement(Anchor(href, *attributes).apply(configuration))
}
class Body(vararg attributes: Attribute) : BodyTag("body", *attributes) // Changed hierarchy
class P(vararg attributes: Attribute) : BodyTag("p", *attributes), RepeatableElement // Repeatable
class Anchor(
href: String? = null,
vararg attributes: Attribute
) : BodyTag("a", *(if (href == null) emptyArray() else arrayOf("href" to href)) + attributes),
RepeatableElement // Repeatable
Right now, this is valid:
html { head { head { title { title { } } } } }
But it produces invalid HTML!
Kotlin’s method resolution automatically searches in “outer” implicit receivers, making the code above equivalent to:
html { this.head { this@html.head { this.title { this@head.title { } } } } }
Inside a DSL, we commonly do not want the implicit receiver to “escape” its scope.
Kotlin provides an annotation used to create scope-blocking annotations:
@DslMarker // I'm defining an annotation that will prevent scope leaking
annotation class HtmlTagMarker // I'm calling it HtmlTagMarker
@HtmlTagMarker // All entities whose this should not get called automatically get annotated
class HTML(vararg attributes: Attribute = arrayOf()) : AbstractTag("html", *attributes) { ... }
@HtmlTagMarker
class Head : AbstractTag("head") { ... }
@HtmlTagMarker // Annotating the common superclass suffices
abstract class TagWithText(name: String, vararg attributes: Attribute) : AbstractTag(name, *attributes) { ... }
With the DslMarker:
html {
head {
// 'fun head(configuration: Head.() -> Unit): Unit' can't be called in this context by implicit receiver.
head { // Compilation Error
title {
// 'fun title(configuration: Title.() -> Unit = ...): Unit' can't be called in this context by implicit receiver.
title { } // Compilation Error
}
}
}
}
You can still do this manually of course:
html { head { this@html.head { title { this@head.title { } } } } } // compiles
But you must be explicitly willing to access an outer receiver
danilo.pianini@unibo.itCompiled on: 2025-11-21 — printable version