DSLs feat. Kotlin

Tyler Walker
5 min readJul 13, 2019

One of the most powerful aspects of Kotlin as a programming language is its extensibility. Kotlin is not your everyday programming language; it has the power to become so much more from where it began — fully extended with advanced, custom-built features and abilities. This extensibility can be taken to such an extent that it is even possible to build a sort of language within a language: namely, a Domain Specific Language, or DSL.

One of the key features that enables this is the flexibility of the Kotlin concept of a “receiver.” The receiver is basically the context of function calls within a block, the “this” to which function calls are applied, and which can be succinctly omitted in Kotlin. When passing a lambda expression as a parameter to some other function or class, we can specify the type of the receiver for that block, so that all function calls executed within the block will be called against the context of that receiver.

Let’s see it in action.

Fundamental Building Blocks

In case you didn’t know, a DSL is basically a syntax or set of operations that make it easier to work within a specific domain. One example might be building up some HTML, or interacting with some backend API. For us, we will be building complex String content that can be displayed on a TextView.

Let’s start by envisioning what this resulting DSL might look like. We want to declaratively nest styles for various segments within a Spannable String. Here’s my vision:

val helloWorld = spannable {
append {
+"Hello"
+" "
red {
+"WORLD!!!!"
}
}
}

Does that make sense? The result should be: “Hello WORLD!!!!” with “WORLD!!!!” in red. But the code is far simpler than what we otherwise would have done, and less prone to mistakes.

So if we break this down, we can see that we will need an atomic operation which allows us to append two Spannables together. Let’s start with a proof of concept of that.

class Atom(val content: SpannableStringBuilder) 

So we’ve said we should have some fundamental unit with some content, which will progressively add operations to. Let’s see what a basic add might look like:

operator fun Atom.plus(other: Atom): Atom = Atom(content.append(other.content))

In Kotlin, we can use the operator keyword to provide implementations for inline operations such as plus. So we’ve declared that plus should return a new Atom with the content of the second Atom appended to the content of the first Atom. Notice this operation is not commutative. Now verify this operation works:

@Test
fun canAddAtoms() {
val first = Atom(SpannableStringBuilder("Hello"))
val middle = Atom(SpannableStringBuilder(" "))
val last = Atom(SpannableStringBuilder("World!"))

val result = first + middle + last

assertTrue(result.content.toString() == "Hello World!")
}

**NOTE** This test has to be run as an Instrumentation test in order to gain access to SpannableStringBuilder.

So this was a proof of concept, but not the end result we were looking for. If you look at the original vision, what we want is an infix operator that we can prefix a string with, appending it to the main body. This operation needs to apply recursively.

class Atom(val content: SpannableStringBuilder) {
operator fun String.unaryPlus() { content.append(this) }
}
fun spannable(block: Atom.() -> Unit) =
Atom(SpannableStringBuilder("")).run {
block()
content
}

Check it out. Hopefully this isn’t too much at once. We extend out class to have a unaryPlus operation that append a String to the main content. That means, if we call String.unaryPlus() within a block where the receiver of that block is an Atom, the String will be appended to the content of that Atom.

That’s what we’ve done in the second function. The function takes a lambda with Atom as receiver. We initialize a blank Atom, execute the block, and return the content. Since we called Atom.run {}, the block will be executed with our blank Atom as receiver. Now we can call it like this:

@Test
fun canAppend() {
val first = "Hello"
val middle = " "
val last = "World!"

val result = spannable {
+first
+middle
+last
}

assertTrue(result.toString() == "Hello World!")
}

Whoa. Pretty cool. Look at the first line. unaryPlus(first) is called with our blank Atom as receiver, and same with the subsequent lines, then the Spannable content is returned at the end of the block.

What if we want some way to nest these operations? We can add another class method to Atom:

class Atom(val content: SpannableStringBuilder) {
operator fun String.unaryPlus() { content.append(this) }
fun append(block: Atom.() -> Unit) =
apply {
block()
}
}

append() is similar to our outer function spannable(), but this time since it is a method of Atom, like unaryPlus() it can be called within the context of a spannable block and will automatically apply the method with the receiver of that block. So we can now nest for the same result:

@Test
fun canDoNestedAppend() {
val first = "Hello"
val middle = " "
val last = "World!"

val result = spannable {
+first

append {
+middle
+last
}
}

assertTrue(result.toString() == "Hello World!")
}

With this basic foundation in place, the sky is now the limit with the types of operations you could add. Let’s add one more for completions sake: a “red” block, which will apply red color to the Strings within that block:

// Atom class method
fun red(block: Atom.() -> Unit) =
apply {
val redPaint = ForegroundColorSpan(Color.rgb(255, 0, 0))

val start = content.lastIndex + 1

block()

val end = content.lastIndex

if (end >= start) {
content.setSpan(redPaint, start, end + 1, Spannable.SPAN_INCLUSIVE_INCLUSIVE)
}
}

Now we have the ability to do all the operations that we initially envisioned.

You can verify everything works by creating a simple activity with a textview and setting its text property to be the spannable content.

Conclusion

This kind of stuff is what makes Kotlin amazing for Android projects. Development of new features becomes much more streamlined. And it’s really fun to build a project with a custom toolset like this. So hope you had fun and some ideas of how you could apply this sort of thing to your own projects.

Complete Gists for above coding samples here:

  1. DSL
  2. Test examples

References:
1. Kotlin documentation regarding DSLs

--

--