dsl

Building blocks of a Kotlin DSL

Introduction

Kotlin is a great general purpose programming language, you can use it to develop applications for almost any kind of platform. But what about cases when a domain-specific language is the better fit for our task? Does Kotlin have tools to change its syntax to create APIs that are more suitable for the problem? Let's take a look!

General-purpose languages vs. DSLs

First, let's clarify the concept of domain-specific languages! A DSL is a language with optimized syntax for a specific application domain. Usually, as a developer, we mainly use a general-purpose language for our work, but we also often interact with DSLs like SQL or HTML. These languages make their users more productive but have limited capabilities. You cannot develop a full-fledged application by using only SQL.

By working with a reduced and optimized set of tools, it's easier to achieve the same result with less code.

Let's check out an example:

You are working on a server application, and you just want to return a simple webpage when a request hits a specific endpoint. You can try to concatenate a string, but it will look ugly and will be hard to maintain.

val html2 = """<html>
    |   <body>
    |       <table>
    |           <tr>
    |               <td>This is a test page generated with:</td>
    |           </tr>
    |           <tr>
    |               <td>Kotlin DSL</td>
    |           </tr>
    |       </table>
    |   </body>
    |</html>""".trimMargin()

Or you can use Kotlin's HTML DSL which is optimized for the task.

val html = createHTML(prettyPrint = true).html {
    body {
        table {
            tr {
                td {
                    text("This is a test page generated with:")
                }
            }
            tr {
                td {
                    text("Kotlin DSL")
                }
            }
        }
    }
}

Much better, isn't it? Even with using Koltin's raw String feature which makes string literals more readable, it's better to have the convenience of type safety and having autocomplete.

Tools to build your DSL

Now we know that what kind of advantages DSL code has over regular general purpose source code, and also that it is possible to use it in Kotlin, let's see the tools we have to create such DSLs!

Lambdas with receivers

Lambdas with receivers are one of the key features of building DSLs with Kotlin. It enables the developer to add structure to the created API.

Let's take a look at apply(), which is a nice helper function, and it's based on lambdas with receivers.

uiLabel.apply {
    textColor = Color.BLACK
    backgroundColor = COLOR.GREEN
    text = ""
}

The apply() function is useful when you want to set multiple properties or address numerous calls on an instance. You don't need to write down the instance variable each time because you have direct access to the instance variable inside the scope of the lambda.

Let's see how the implementation of the apply function looks like:

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

As you can see, apply() is an extension function on a generic type T. The key to understanding how it works is to see that it receives a function type as a parameter:
block: T.() -> Unit

But it's not a regular function type. It's called an extension function type, which means that basically, it is an extensions function in the form of a lambda expression. It has the advantage of a regular extension function: inside the function scope, the programmer can access variables and functions of the extended type.

Infix calls

The infix call is a great feature that allows us to write our code as if we would write regular sentences. By eliminating dots and parentheses, syntax gets clearer, so it's a better fit for our plans to write more DSL like code. Let's see how an infix call looks like:

infix fun String.concat(other:String) = "$this$other"

val a = "a"
val b = "b"

val c = a concat b

We just have to put the infix notation before the defined function signature, and than it is ready for use. Although there are some limitations:

  • The function can only be an extension or a member function.
  • The function must have a single parameter.
  • The parameter must have a fixed number of arguments and no default values for those.

But if all these requirements are met, we can easily create APIs with a more natural and expressive syntax.

Invoke

Maybe you are already familiar with the invoke call in case of lambdas. You can use to address a safe call to an optional function type like this:

lambda?.invoke()

But normally you can call a lambda, lambda(), which is just another form of invoking an object.

You can define an invoke() function on a class like this:

class Family {
        operator fun invoke() {
          //do something
        }
    }

After that, we can invoke an instance of a Family type like we call a function.

val family = Family()
family()

But how is that useful if we want to create a DSL? Let's see an example:
We want to add members to our family, but we want the syntax to be flexible. Classic flat structure and a nested structure should also be available for usage. Now a previously learned technique comes into play: lambdas with receivers.

First, let's extend the Family class.

class Family {
    fun addMember(name: String) {}

    operator fun invoke(body: Family.() -> Unit) {
        body()
    }
}

Now we use an extension function type as a parameter for an invoke function, which allows us to use a nested DSL syntax when we want to add members to our family:

family {
    addMember("Mom")
    addMember("Dad")
    addMember("Kid")
}

Also, the flat structured call is still available:

family.addMember("Mom")

Summary

Kotlin is packed with features allow us, developers, to make great DSLs which simplify our work. If you think about your everyday tasks, I think you can find many topics where you can benefit from a declarative DSL-like syntax. JetBrains also gave us nice examples of DSL usage, like the HTML builder, or Anko DSL. The community is already started to recognize the possibilities of this great feature - if you are an Android developer take a look at this lib. It's your turn now :)