danilo.pianini@unibo.it
Compiled on: 2024-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:
implicit
s, 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 interface
s,
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>
Element
sElement
s can be either Tag
s or plain Text
Element
s can be Repeatable
Text
elements are always Repeatable
Tag
s can be Repeatable
interface Element
interface RepeatableElement : Element
interface Tag : Element
interface RepeatableTag : Tag, RepeatableElement
interface TextElement : RepeatableElement
Element
s can be rendered to plain text, possibly with some indentationinterface Element {
fun render(indent: String = ""): String
}
TextElement
s 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 Element
s, and key/value attributes
interface Tag : Element {
val name: String
val children: List<Element>
val attributes: Map<String, String>
}
String
as indentationPair
s is not very clean:
we convey a better meaning to what we are doing by relying on a typealias Attribute
TextElement
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
Tag
Tag
is a bit more complex, let’s try to factor the common part of all Tag
s into an AbtractTag
name
is easyAttribute
s get registered at creation time. We use a vararg
to make the construction nicerchildren
register
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.it
Compiled on: 2024-11-21 — printable version