Small introduction
Hello! I often hang out on Medium and find a lot of useful articles from foreign developers. On one of these days, I was looking for something on DSL at Kotlin for myself and came across a series of articles on what DSL is at Kotlin and how to work with it. Before reading, I had a superficial notion of DSL, since I occasionally came across them. While reading the article, I liked the simplicity of the description and the submission of examples from the author so that at the end of the reading I decided to translate this pair of articles for you. Of course, with the approval of the author :) Well, let's start.
Briefly about DSLs. What is it?
For starters, you can spy the definition from Wikipedia
Domain-specific language (DSL) is a computer language specialized for a specific application. It differs from general purpose languages (GPL), which are widely used in many areas.
In principle, DSL is a language that focuses on one particular part of an application; on the other hand, general purpose languages such as Kotlin and Java can be used in other parts of the application. There are several DSL with which you probably already know, for example, SQL. If you look at SQL, you can see that it looks almost like a regular sentence in English, so that it becomes quite
readable ,
understandable and
expressive :
SELECT Person.name, Person.age FROM Person ORDER BY Person.age DESC
There are no special criteria that would distinguish a DSL from a normal API, but we often notice one difference: The use of a specific structure or grammar. This makes the code more understandable for a person who is easy to understand, not only for developers, but also for people who are less savvy in terms of programming languages.
')
DSLs with Kotlin
Now, let's figure out how we can create DSL with some Kotlin language features and what advantages does this bring to us?
When we create DSL in some universal programming language, such as Kotlin, we actually mean
internal DSL . After all, we do not create an independent syntax, but simply set up a specific way to use this language. And it is this that gives us the advantage of using code that we already know, and allows us to add different operators, such as
for loops, to our DSL.
In addition, Kotlin offers several ways to create a cleaner syntax and helps to avoid using too many unnecessary characters. And in this first part we will look at three features:
- Using lambdas outside method brackets
- Lambda with arguments (receivers)
- Extension functions (from the translator: these are the Extension functions, which everyone has heard about, probably)
How to use it all - it will become clearer in a minute when we create several examples.
To make everything as clear as possible, in this part I will use a simple model to create our DSL. We should not create DSL when creating a class. That would be superfluous. A good place to use DSL can be
a configuration class or
a library interface , where the user does not need to know about the models.
Now let's write our first DSL
In this section, we will create a simple DSL that can create an object of class
Person . Note - this is just
an example . So here is an example of what we are going to get at the end of this lesson:
val person = person { name = "John" age = 25 address { street = "Main Street" number = 42 city = "London" } }
You can immediately notice that the code above itself describes itself and is easy to understand. Even a person who has no developer experience can read this and even make his own edits. To understand how we can recreate this, we will take a few steps. Here is the model with which we start:
data class Person(var name: String? = null, var age: Int? = null, var address: Address? = null) data class Address(var street: String? = null, var number: Int? = null, var city: String? = null)
Obviously, this is not the purest model we can write. But we want to have
immutable properties (val) . And before that we will get in the next parts of this series.
The first thing we do is create a new file. In it we will keep the DSL separately from the actual classes in our model. Let's start by creating some constructor function for our
Person class. Looking at the result we want to have, we see that all the properties of the
Person class are defined in the code block. But in fact, these braces mean lambda. Here we use the first of the three above-mentioned features of the Kotlin language:
Using lambdas outside the brackets of the method .
If the last parameter of the function is lambda, then we can use it outside the brackets. And if you have only one parameter that is lambda, then you can completely remove the brackets. This means that
person {...} is the same as
person ({...}) . This leads to less syntactic pollution in our DSL. Now we will write the first version of our function.
fun person(block: (Person) -> Unit): Person { val p = Person() block(p) return p }
So. Here we have a function that creates a Person object. This requires a lambda, which has an object that we create in line 2. When we execute this lambda in line 3, we expect the object to receive the necessary properties for it before we return the object in line 4. Now let's see how we can use the function we wrote:
val person = person { it.name = "John" it.age = 25 }
Since lambda takes only one argument, we can access the person through
it . It looks pretty good, but this is not the end. In fact, this is not exactly what we want to see in our DSL. Especially when we are going to add additional layers of objects. This leads us to the next Kotlin function we mentioned:
Lambdas with arguments (receivers) .
In the definition of the person function, we can add a
receiver to the lambda. Thus, we can access only the functions of this receiver in this lambda. Since the functions are in the receiver's domain, we can simply execute the lambda on the receiver instead of substituting it as an argument for the lambda.
fun person(block: Person.() -> Unit): Person { val p = Person() p.block() return p }
And yet it can be written easier. For example, using the
apply function provided by Kotlin.
fun person(block: Person.() -> Unit): Person = Person().apply(block)
Now we can remove
it from our DSL.
val person = person { name = "John" age = 25 }
Looks great, right? :) We are almost done. But we missed one point - the
Address class. In our desired result, it is very similar to the
person function we just created. The only difference here is that we have to assign it as the
address for the Person object. In order to do this, we will use the last of the three Kotlin functions mentioned above:
Extension functions .
Extension functions give you the ability to add functions to classes without access to the source code of the class itself. This is ideal for creating an
Address object and assigns it directly to the
address in
Person . Here is the final version of our DSL file (for now).
fun person(block: Person.() -> Unit): Person = Person().apply(block) fun Person.address(block: Address.() -> Unit) { address = Address().apply(block) }
We added the
address function to
Person , which takes a lambda with
Address as a receiver, just like we did with the
person constructor function. Then it sets the created
Address object in the property of the
Person class. Now we have created a DSL to create our model.
val person = person { name = "John" age = 25 address { street = "Main Street" number = 42 city = "London" } }
This is the first part of a long series on how to write DSLs in Kotlin. In the second part, we’ll talk about adding collections, using the Builder template and @DslMarker annotation. There is a real-life example using GsonBuilder.
You can find the original article here:
Writing DSLs in Kotlin (part 1)Author of the original series of articles:
Fré Dumazy