All Articles

How do Kotlin DSL libraries work?

Article cover

As Kotlin becomes more popular, we will keep getting more libraries that offer “idiomatic” features. Many of them heavily use DSLs. We’ll see how they work and how can you implement your own.

What is a DSL?

Before diving into definitions, let’s start by looking a samples of Kotlin DSLs

Anko Layouts

verticalLayout {
    val name = editText()
    button("Say Hello") {
        onClick { toast("Hello, ${name.text}!") }
    }
}

kotlinx.html

createHTML().html {
  head { +"Hello!" }
  body {
    p { +"This is a DSL!"}
  }
}

Spek (testing framework)

object MyTest: Spek({
    group("a group") {
        test("a test") { ... }
        group("a nested group") {
            test("another test") { ...}
        }
    }
})

Recognize any of those? Looks as if they are built-in language features. However, they are all extensions of the standard Kotlin syntax that you use everyday!

What is an (external) DSL?

Domain specific language (DSL) is a computer language that’s targeted to a particular kind of problem, rather than a general purpose language that’s aimed at any kind of software problem. (source: Martin Fowler)

Basically, given an specific problem or domain that we are dealing with, we define a language that can help us code a solution. SQL solves database querying, CSS solves styling, and so on.

What is an (internal) DSL?

Given a programming language that we already know and love (Kotlin), and that has powerful, functional capabilities, we can implement our own solution that can help us reduce boilerplate and create declarative solutions to a common problem.

Martin Flower also describes them as fluent interfaces. They are amazing when well-implemented, however, coming up with a nice fluent API requires a good bit of thought, as he says.

If you take a look at Fowler’s original posts, you’ll see that those are not new concepts: they’ve been around for decades now!

How are Kotlin DSLs implemented?

Most Kotlin DSLs are implemented by using Kotlin’s lambdas and higher order functions.

In Kotlin, we can pass functions as parameters to other functions:

fun execute(id: String, body: ()->Unit) { println("---- $id starts") body() println("---- $id finishes") } fun main() { execute(id = "main", body = { println("Hello world") }) }

However, that is a very verbose way of calling the execute function.

  1. The named parameter body is not needed
  2. In Kotlin, when the last parameter of our function is another function, we can lift it out of the parenthesis
fun execute(id: String, body: ()->Unit) { println("---- $id starts") body() println("---- $id finishes") } fun main() { //sampleStart execute("main") { println("Hello world") } //sampleEnd }

There’s nothing that stops us from nesting the calls

fun execute(id: String, body: ()->Unit) { println("---- $id starts") body() println("---- $id finishes") } fun main() { //sampleStart execute("main") { execute("hello") { println("Hello ") } execute("world") { println("world") } } //sampleEnd }

Take a look at it: it already looks a bit like the DSLs we saw at the beginning!

Keeping state on our code

The examples above just execute blocks of code, however it is not storing any kind of information, nor allows us to create any kind of data structure.

That’s a pretty common case for DSLs, so let’s define a simple EditText class

data class EditText(
  var text: String = "",
  var hint: String = "",
  var onTextChanged: (String)-> Unit = {}
)

Our (incredibly simple) DSL would allow us to setup its value this way:

editText {
  text = "Hello"
  hint = "Enter something"
  onTextChanged { text -> print("You wrote: $text") }
}

We can do something similar by using apply standard function

data class EditText( var text: String = "", var hint: String = "", var onTextChanged: (String)-> Unit = {} ) fun main() { //sampleStart EditText().apply { text = "Hello" hint = "Enter something" onTextChanged = { text: String -> print("You wrote: $text") } } //sampleEnd }

Close enough, but not exactly what we want. What is apply doing behind the scenes to allow us to assign our variables that way? (code below is simplified)

public inline fun T.apply(block: T.() -> Unit): T { block() return this }

It’s receiving a parameter of type T.() -> Unit. Notice how the definition is T.() instead of (T).

This is called a lambda with receiver (sometimes called “extension lambdas”). Lambdas with receivers basically work as extension functions. Whatever you write within that lambda, will be executed within the type T scope.

Play with the code below to see the difference between a lambda that receives an object as parameter, and one that uses lambdas with receivers.

Object as parameter

data class EditText( var text: String = "", var hint: String = "", var onTextChanged: (String)-> Unit = {} ) fun main() { //sampleStart fun execute(body: (EditText) -> Unit) { body(EditText()) // as parameter } execute { it.text = "hi" text = "hi" // won't work } //sampleEnd }

Lambda with receiver

data class EditText( var text: String = "", var hint: String = "", var onTextChanged: (String)-> Unit = {} ) fun main() { //sampleStart fun execute(body: EditText.() -> Unit) { EditText().body() // we execute as an extension function }

execute { this.text = “hi” text = “hi” // same as above it.text = "" // won’t work } //sampleEnd }

Now you know how to create extension functions, so we have the tools to achieve the DSL we want. Think about how would you do it for a minute, and then expand the snippet below to see a possible solution.

data class EditText( var text: String = "", var hint: String = "", var onTextChanged: (String)-> Unit = {} )

fun editText(body: EditText.()->Unit ): EditText { return EditText().apply { body() } }

fun main() { //sampleStart editText { text = “Hello” hint = “Enter something” onTextChanged = { text -> print(“You wrote: $text”) } } //sampleEnd }

We have a small problem, though. For simplicity, we defined onTextChanged as public and mutable (var). However, that is not correct because:

  • We should NOT be able to invoke it from outside. The EditText itself should decide when to emit that event.
  • Notice that we have to do onTextChanged = (= at the end). This is not how we initially defined it on our expected DSL.

There is no magic involved to achieve that, you can use everything we learned here! Once again, think about how you would do it and then check the solution

data class EditText( var text: String = "", var hint: String = "" ) { private var listener: (String)-> Unit = {}

fun onTextChanged(listener: (String)-> Unit) { this.listener = listener } }

fun editText(body: EditText.()->Unit ): EditText { return EditText().apply { body() } }

fun main() { //sampleStart editText { text = “Hello” hint = “Enter something” onTextChanged { text -> print(“You wrote: $text”) } } //sampleEnd }

Awesome, we did it! We modified a few things on the class itself, and we achieved the results that we were looking for.

DSL on top of code that is not ours

But what if we don’t have access to its source code, so we can’t change it? That’s a very common scenario when you build DSLs. Anko for example can’t modify the Android components source code, so what do they do?

The answer is: builders and extension functions.

  • Builders: our DSL will modify the properties of the builder, and at the end our builder will use those values to create the external object (be calling its setters)
  • Extension functions: builders are not necessary if your DSL is used to execute calls nicely rather than to create data structures. A common example for this, would be a DSL to make it easy to create database transactions. Extension functions and lambdas with receivers will work as wrappers for the external components functionality.

We won’t see examples of those, but why not give it a shot and try to do one of your own?

Final notes

  • A good understanding of how functions work in Kotlin will help us implement our own DSL.
  • Even if you don’t want or need to implement your own DSL, you’ll mostly likely use one at some point. It’s always good to know how your tools work internally.
  • You can also contribute to existing open source projects!
  • Good DSLs require a little bit of thought. Keep in mind that not everything needs to be a DSL.