DSL (Domain-specific language) - language specialized for a specific application ( Wikipedia )
The article “ Why Kotlin sucks, ” in which the author complains that Kotlin “has no syntax for describing structures,” pushed me to write this post. For some time programming on Kotlin I got the impression that if it is impossible in it, but I really want it, then it is possible. And I decided to try to write my DSL to describe the data structure. That's what came out of it.
Despite the fact that I would like to really get DSL for describing the structures, the purpose of the article I first of all want to put the explanation (with examples) of the features of the Kotlin language, with the help of which writing this DSL itself becomes possible. Well, a simple DSL, of course, we will write :)
For simplicity, or because of some personal preference, I want the syntax of my future DSL to describe the data structure to be similar to JSON. In short, the syntax implies the following:
We need to start from something and we will start by forcing us to compile an empty structure of the form:
struct { }
It's not hard to do this, you just need to declare a function
fun struct(init: () -> Unit){ }
The struct(...)
function takes as a parameter another function that returns Unit
and so far does nothing more. But this function reveals to us an important Kotlin chip, which will help us in writing DSL: if the last argument of the function is another function, then it can be declared outside the brackets "(...)". If a function has only 1 argument, and this argument is a function, then parentheses can be omitted.
Thus, our struct {}
code is equivalent to the struct({})
code, only shorter.
Well, we have an empty structure! Actually, no, we only have a struct
function that does not even return anything. It is necessary that she returned at least something:
class Struct // , Kotlin fun struc(init: () -> Unit) : Struct { return Struct() } fun main() { val struct = struct { } }
Now we really have some empty object of the Struct class.
It's time to add some content. I tried to find a way to make the view construct work.
struct { "field1": 1, "field2": 2 }
I could not achieve an exact match, but it turned out to make as many as 3 alternative syntaxes, which, if desired, can be used simultaneously :)
struct { s("field1" to 1) s("field2" to arrayOf(1, 2, 3)) s("field3" to struct { s("field3.1" to 31) }) } struct { +{ "field1" to 1 } +{ "field2" to 2 } +{ "field3" to struct { +{ "field3.1" to 31 } } } } struct( "field1" to 1, "field2" to 2, "field3" to struct( "field1.1" to 11 ) )
Note that in the third case it was necessary to use parentheses, not curly ones, but in it the least number of characters.
So how to make it work? First, the data in the Struct class must be stored somewhere. I chose hashMap<String, Any>()
, since the structure field is a string, and the value is any object.
class Struct { val children = hashMapOf<String, Any>() }
Secondly, this data needs to be added somehow to the structure. Recall that everything inside the curly braces after the word struct
is a function that we passed to the struct(...)
argument. So, in order to manipulate a Struct
object Struct
we need to access this object inside the passed function. And we can do it!
fun struct(init: Struct.() -> Unit): Struct { val struct = Struct() struct.init() return struct }
We changed the type of the init
function to Struct.() -> Unit
. This means that the function passed must be a function of the Struct
class or its extension function. With such a function declaration, we can perform struct.init()
, and this, in turn, means that inside the init()
function, we will access an instance of the Struct
class through, for example, this
.
For example, now we have the right to write such code:
struct { this.children.put("field1", 1) // this - Struct, struct() }
It already works, but it is a little similar to language of the description of data structure. Add support for designs
struct { +{ "field1" to 1 } }
"field1" to 1
is the equivalent of Pair<String, Any>("field1", 1)
. It is wrapped in braces, which is a lambda function. The last line of the lambda function defines the type of value it returns, and the value itself. In other words, { "field1" to 1 }
is a lambda that returns Pair<String, Any>
.
With lambda finished, but what is this "+" in front of her? And this is a redefined unary operator "+", by calling which we add the pair obtained from the lambda to our structure. Its implementation looks like this:
class Struct { val children = hashMapOf<String, Any>() operator fun (() -> Pair<String, Any>).unaryPlus() { // + val pair = this.invoke() // children.put(pair.first, pair.second) // } }
Next, let's deal with the support of the syntax of the form:
struct { s("a" to 2) }
There are no lambdas here, immediately creating the Pair
object and some kind of "s" in front of it. Actually "s" is also an operator, but already infix. Where did he come from? So I wrote it myself, here it is:
class Struct { val children = hashMapOf<String, Any>() infix fun Struct.s(that: Pair<String, Any>): Unit { this.children.put(that.first, that.second) } }
It returns nothing, but adds the pair passed to it in our data structure. The letter "s" I chose just like that, the name of the operator can be any. By the way, to
in the expression "field1" to 1
is also an infix operator that returns a Pair("field1", 1)
Finally, add support for the third syntax. The most concise, but the most boring in terms of implementation.
struct( "field1" to 1 )
It's not hard to guess that "field1" to 1
is just an argument to the function struct(...)
. To be able to pass multiple pairs, we will declare this argument as vararg
fun struct(vararg data: Pair<String, Any>, init: Struct.() -> Unit): Struct { val struct = Struct() for (pair in data) { struct.children.put(pair.first, pair.second) } struct.init() return struct }
We have learned to describe the structure, but it is not worth a damn if we don’t give an opportunity to work with it. We don't want to write code like this: struct.children.get("field")
, we don’t want to know anything about children
. We want to immediately refer to the fields of our structure. For example, like this: val value = struct["field1"]
. And we can teach our DSL this trick if we define another operator for our Struct class :)
class Struct { val children = hashMapOf<String, Any>() operator fun get(s: String): Any? { return children[s] } }
Yes, this is the "get" operator (namely the operator , not the getter), which is automatically called when the object is accessed using square brackets.
We can say that we did DSL. Let not perfect, with obvious shortcomings in the form of the impossibility to automatically deduce the type of each field, but it turned out. Probably, if you practice for some time, you can find ways to improve it. Maybe readers have ideas?
An example of the code can be viewed at the link.
Source: https://habr.com/ru/post/322372/
All Articles