Internal DSL's and Kotlin

It’s safe to claim that programming languages, in a very broad sense, could be regarded as means to solve a myriad of problems, however sometimes it feels like some common issues are more specific than others, and that’s where a DSL fits in as it concerns addressing problems from a specific domain with simple and human readable code.

External and Internal DSL’s

External DSL’s have their own syntax and can be run independently, for instance SQL for relational data manipulation, Gherkin for testing and HTML for web development are considered external DSL’s. Internal DSL’s on the other hand are written on top of general purpose languages; examples include Spring Security configuration & Exposed, MyBatis and jOOQ support for working with SQL fluently. This topic focuses on internal DSL’s and how Kotlin allows to write these with a readable and clean syntax.

A Kotlin DSL can look like this simple HTTP security config:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
val settings = settings {
    secretKey = "mySecretKey"
    publicResources {
        publicResource {
            method = "GET"
            urls = "/public,/test"
        }
        publicResource {
            method = "POST"
            urls = "/user"
        }
    }
}

Higher-order functions

High-order functions are similar to their regular counterpart except that they can take one or more functions as arguments or return a function, it is also worth noting that in Kotlin functions are first-class, which means they can be stored and assigned as a variable and as such passed as a regular parameter. This is a very important concept since we rely on passing lambda expressions as parameters when creating our DSL’s and so on.

1
2
3
4
5
6
7
8
9
fun sum(a: Int, b: Int) = a + b
val sum: (Int, Int) -> Int = { a: Int, b: Int -> a + b }
fun fold(values: List<Int>, op: (a: Int, b: Int) -> Int) = values.reduce(op)

fun main() {
    fold(listOf(10, 14), ::sum)
    fold(listOf(10, 14)) { a, b -> a + b } //lambda outside of parentheses
    fold(listOf(10, 14), sum)
}

Lambdas outside of parentheses

This feature allows us to write lambda expressions outside of parentheses, thus making such DSL syntax possible, if you take a look at the previous code snippet you can see that we already applied this. It is valid to use this kind of syntax when the last argument of your function expects a lambda, and if that is your function single argument you can skip the parentheses altogether, which means that if our function fold from the aforementioned snippet where to receive a single lambda we could do something like in the following example:

1
2
3
4
5
fun fold(op: (a: Int, b: Int) -> Int) = (0..10_000).reduce(op)

fun main() {
    fold { a, b -> a + b }
}

Lambdas & lambdas with receivers

Now let’s take a look at a simple function that receives a lambda as parameter:

1
2
3
4
5
6
7
8
fun settings(block: (JwtAuthenticationSettingsBuilder) -> Unit) = JwtAuthenticationSettingsBuilder()
        .apply(block)
        .build()

val s = settings {
    it.secretKey = "mySecretKey"
    //...
}

Even though this syntax looks clean and concise it requires the use of the it prefix inside the lambda in order to refer to the underlying attributes, to further improve readability and cast aside the need of the redundant it identifier we can refactor our settings() function to work with a lambda with receivers, this will allow access to our DSL attributes directly.

1
2
3
fun settings(block: JwtAuthenticationSettingsBuilder.() -> Unit) = JwtAuthenticationSettingsBuilder()
        .apply(block)
        .build()

Receivers

If you take a look at the previous examples you’ll notice that we dropped the usage of (JwtAuthenticationSettingsBuilder) -> Unit in favor of JwtAuthenticationSettingsBuilder.() -> Unit, this way turning our ordinary lambda into a lambda with receiver, in this case the receiver type is identifier before .(), and it’s basically the type on which we are trying to reach members without an explicit qualifier, this is very similar to the concept of extension functions.

Receiver Types [source]

Receiver Types [source]

Type-safe builders

When our DSL’s have more complex types such as lists, maps or objects from user defined classes we can make use of builders to help writing and using our DSL’s.

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
@DslMarker
private annotation class JwtSettingsDsl
...
@JwtSettingsDsl
class PublicResources : ArrayList<PublicResource>() {

    fun publicResource(block: PublicResourceBuilder.() -> Unit) = add(PublicResourceBuilder().apply(block).build())

}

@JwtSettingsDsl
class PublicResourceBuilder {

    var method: String? = null
    lateinit var urls: String

    fun build() = PublicResource().apply {
        method = this@PublicResourceBuilder.method.let(HttpMethod::resolve)
        urls = this@PublicResourceBuilder.urls.split(",").map(String::trim)
    }

}

In this example we have PublicResourceBuilder which is responsible to build our PublicResource objects, PublicResources collects PublicResourceBuilder instances to a list. @DslMarker is used to narrow the scope of our receivers and prevent then from accessing data from outer lambdas.

Code from this post can be found on GitHub

Thanks :)

Source:

https://martinfowler.com/books/dsl.html

https://www.manning.com/books/kotlin-in-action