Reputation: 15336
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
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:
this
parameter of a specific type to your lambda.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