Alex Craft
Alex Craft

Reputation: 15336

Is it possible to easily build DSL in Kotlin?

TypeScript allows very nice and clean and 100% type-safe way to build data-like DSLs. I wonder if it's possible in Kotlin?

For example, in TypeScript code below (playground) we defining columns for data table. It checks that values are correct (string enums), checks all the optional / required fields, has autocomplete etc. And it just works out of the box, all you need to do is define types.

Is it possible to use something like that in Kotlin? It's possible to use Java Builder-pattern, but it's not ideal, and it requires to write lots of code for builder-methods. Also, Kotlin doesn't have a way to use "number" enum, it would be Type.number, doesn't look nice. Or maybe I'm missing something and there's a way to build nice and clean DSL in Kotlin without too much boilerplate code?

// Defining DSL ---------------------------------------------
type Type = "string" | "number" | "boolean" | "unknown"

interface StringFormatOptions {
  type: "string"
}

interface LineFormatOptions {
  type:   "line"
  ticks?: number[]
}

interface Column {
  type:    Type
  format?: StringFormatOptions | LineFormatOptions
}


// Using DSL ------------------------------------------------
const columns: Column[] = [
  {
    type:  "number",
    format: { type:  "line", ticks: [1000] }
  },
  {
    type:  "string"
  }
]

Upvotes: 2

Views: 683

Answers (1)

broot
broot

Reputation: 28362

Yes, you can create type-safe DSLs in Kotlin. It may be tricky to understand at first, but it really become very easy when you get used to it.

It works by creating functions that receive lambdas which have a specific receiver type... Well... let's try again. Assuming you are the user of already existing DSL, this is what happens:

  1. There is a function that requires providing a lambda to it.
  2. You provide a lambda.
  3. The function provides a this parameter of a specific type to your lambda.
  4. You can use properties/functions of provided this object in the lambda, effectively making possible to go deeper into DSL chain.

Let's see this example:

fun copy(init: CopyBuilder.() -> Unit) { TODO() }

interface CopyBuilder {
    var from: String
    var to: String

    fun options(init: CopyOptionsBuilder.() -> Unit) { TODO() }
}

interface CopyOptionsBuilder {
    var copyAttributes: Boolean
    var followSymlinks: Boolean
}

We have a copy() function which receives a lambda. Provided lambda will have access to CopyBuilder object as this, so it will have access to e.g. from and to properties. By calling options() from the lambda we move deeper and now we have access to CopyOptionsBuilder object.

copy() is responsible for providing a proper implementation of CopyBuilder object to your lambda. Similarly, implementation of options() need to provide a proper implementation of CopyOptionsBuilder. This was omitted from the example above.

Then it can be used as:

copy {
    from = "source"
    to = "destination"

    options {
        copyAttributes = true
        followSymlinks = false
    }
}

If you use Gradle with Kotlin DSL then build.gradle.kts file is actually a regular Kotlin file. It just starts with some variables provided to you. Another good example of DSL in Kotlin is kotlinx.html library. It generates HTML code with syntax like this:

html {
    body {
        div {
            a("https://kotlinlang.org") {
                target = ATarget.blank
                +"Main site"
            }
        }
    }
}

You can read more about this here: https://kotlinlang.org/docs/type-safe-builders.html

Upvotes: 3

Related Questions