Effective Kotlin Item 35: Consider defining a DSL for complex object creation
A set of Kotlin features used together allows us to make a configuration-like Domain Specific Language (DSL). Such DSLs are useful when we need to define more complex objects, or a hierarchical structure of objects. They are not easy to define, but once they are defined, they hide boilerplate and complexity and a developer can express his or her intentions more clearly.
For instance, Kotlin DSL is a popular way to express HTML: both classic HTML, and React HTML. This is how it can look like:
View from the above HTML DSL
Views on other platforms can be defined using DSLs as well. Here is a simple Android view defined using the Anko library:
View from the above Android View DSL
Similarly, with desktop applications. Here is a view defined on TornadoFX that is built on top of the JavaFX:
View from the above TornadoFX DSL
DSLs are also often used to define data or configurations. Here is API definition in Ktor, also a DSL:
Here are test case specifications defined in Kotlin Test:
We can even use Gradle DSL to define Gradle configuration:
Creating complex and hierarchical data structures become easier with DSLs. Inside those DSLs we can use everything that Kotlin offers, and we have useful hints as DSLs in Kotlin are fully type-safe (unlike Groovy). It is likely that you already used some Kotlin DSL, but it is also important to know how to define them yourself. Not only to do that in the future, but also to better use DSLs.
Defining your own DSL
To understand how to make your own DSLs, it is important to understand the notion of function types with a receiver. Before that, we’ll first briefly review the notion of function types themselves. The function type is a type that represents an object that can be used as a function. For instance, in the
filter function, it is there to represent a predicate that decides if an element can be accepted or not.
Here are a few examples of function types:
()->Unit- Function with no arguments that returns
(Int)->Unit- Function that takes
(Int)->Int- Function that takes
(Int, Int)->Int- Function that takes two arguments of type
(Int)->()->Unit -Function that takes
Intand returns another function. This other function has no arguments and returns
(()->Unit)->Unit- Function that takes another function and returns
Unit. This other function has no arguments and returns
The basic ways to create instances of function types are:
Using lambda expressions
Using anonymous functions
Using function references
For instance, think about the following function:
Analogous functions can be created in the following ways:
In the above example, property types are specified and so argument types in the lambda expression and in the anonymous function can be inferred. It could be the other way around. If we specify argument types, then the function type can be inferred.
Function types are there to represent objects that represent functions. An anonymous function even looks the same as a normal function, but without a name. A lambda expression is a shorter notation for an anonymous function.
Although if we have function types to represent functions, what about extension functions? Can we express them as well?
It was mentioned before that we create an anonymous function in the same way as a normal function but without a name. So anonymous extension functions are defined the same way as well:
What type does it have? The answer is that there is a special type to represent extension functions. It is called function type with receiver. It looks similar to a normal function type, but it additionally specifies the receiver type before its arguments, and they are separated using a dot:
Such a function can be defined using a lambda expression, specifically a lambda expression with receiver, since inside its scope the
this keyword references the extension receiver (an instance of type
Int in this case):
Object created using anonymous extension function or lambda expression with receiver can be invoked in 3 ways:
- Like a standard object, using
- Like a non-extension function.
- Same as a normal extension function.
The most important trait of the function type with receiver is that it changes what
this refers to. To see how this trait can be used, think of a class that needs to be set property by property:
Referencing to the dialog repeatedly is not very convenient, but if we would use a lambda expression with receiver, it would be
this, and we would be able to just skip it (because receiver can be used implicitly):
Following this path, someone might define a function, that takes all the common parts of dialog creation and showing, and leaves to the user only properties setting:
This is our simplest DSL example. Since most of this builder function is repeatable, it has been extracted into an
apply function, that can be used instead of defining a DSL builder for setting properties.
Function type with a receiver is the most basic building block of Kotlin DSL. Let’s create a very simple DSL that would allow us to make the following HTML table:
Starting from the beginning of this DSL, we can see a function
table. We are at top-level without any receivers, so it needs to be a top-level function. Although inside its function argument you can see that we use
tr function should be allowed only inside the table definition. This is why the
table function argument should have a receiver with such a function. Similarly, the
tr function argument needs to have a receiver that will contain a
How about this statement:
What is that? This is nothing else, but a unary plus operator on String. It needs to be defined inside
Now our DSL is well-defined. To make it work fine, at every step we need to create a builder and initialize it using a function from parameter (
init in the example below). After that, the builder will contain all the data specified in this
init function argument. This is the data we need. Therefore, we can either return this builder, or we can produce another object holding this data. In this example, we’ll just return builder. This is how the
table function could be defined:
Notice that we can use the
apply function, as shown before, to shorten this function:
Similarly, we can use it in other parts of this DSL to make them more concise:
This is a simple (but functional) DSL builder for HTML table creation. It could be improved using a
DslMarker explained in Item 15: Consider referencing receiver explicitly.
When should we use it?
DSLs give us a way to define information. It can be used to express any kind of information you want, but it is never clear to users how this information will be later used. In Anko, TornadoFX or HTML DSL we trust that the view will be correctly built based on our definitions, but it is often hard to track how exactly. Some more complicated uses can be hard to discover. Usage can be also confusing to those not used to them. Not to mention maintenance. The way how they are defined can be a cost - both in developer confusion and in performance. DSLs are an overkill when we can use other, simpler features instead. Though they are really useful when we need to express:
- complicated data structures,
- hierarchical structures,
- huge amount of data.
Everything can be expressed without DSL-like structure, by using builders or just constructors instead. DSLs are about boilerplate elimination for such structures. You should consider using DSL when you see repeatable boilerplate code1 and there are no simpler Kotlin features that can help.
A DSL is a special language inside a language. It can make it really simple to create complex object, and even whole object hierarchies, like HTML code or complex configuration files. On the other hand DSL implementations might be confusing or hard for new developers. They are also hard to define. This is why they should be only used when they offer real value. For instance, for the creation of a really complex object, or possibly for complex object hierarchies. This is why they are also preferably defined in libraries rather than in projects. It is not easy to make a good DSL, but a well-defined DSL can make our project much better.
Repeatable code not containing any important information for a reader