Kotlin is a powerful, safe and laconic language. But sometimes it’s possible to make a code even shorter and more readable using your own DSL. In this article I’m going to tell how to implement your own DSL in Kotlin using higher-order functions.
What is DSL?
DSL (domain-specific language) is a simple programming language specifically designed for a particular domain. Unlike general-purpose languages, which may be widely used across domains, the domain-specific languages usually are quite simple with limited possibilities and represent some very specific area.
The basic idea of a domain specific language (DSL) is a computer language that’s targeted to a particular kind of problem, rather than a general purpose language that’s aimed at any kind of software problem. Domain specific languages have been talked about, and used for almost as long as computing has been done
– Martin Fowler
In other words, DSL is a markup language created to define data structure, operations flow or configuration blocks. It is sort of a tiny scripting programming language.
DSLs are very common in computing:
OpenGl Shading Language,
user interface markup languages, many other definitions, including CI servers job configuration languages.
Below, I’ll describe how to make a logging domain-specific language. It should help us to configure a log record in a nice, readable and fancy way. Also, I’ll try to discover other use cases of DSL and at the end of the article you can find a list of libraries from Kotlin world, which already implement DSL for different purposes.
As I mentioned above, DSL can be easily implemented in Kotlin using higher-order function and function literal with receiver as parameter.
Higher-ordered function – function that takes another function as parameter
Function literal with receiver – lambda with super-power of extensions functions
The function as parameter receives another function that receives object type
T and returns
action is a function literal, non-defined function, but without a receiver. To transform
action into lambda with receiver we need to define it in an extension-like way.
Now, while calling the
foreach function, you need to pass lambda as an argument and inside that lambda you can call the methods of
T without any qualifiers.
Implementation of logging DSL
This sample includes abstraction layer for logging libraries and DSL implementation itself. I’ll briefly touch upon the abstraction layer. This is just
enum with levels and interface with one
log function that takes
throwable as parameters. That’s it, keep it simple.
The purpose of DSL is making simple language to configure one message to be logged. So, the first step is writing a class to represent configuration.
The next step is adding functions to configure those 4 attributes of the log message. As we’ve seen, to use those functions in DSL way, we need to define them as higher-level with lambda with receiver as parameter.
Additionally, I defined functions and parameters as
crossinline respectively. That helps to decrease memory allocations and method call stack overhead. In this case, lambda won’t be created as an object of
Function0 class. Instead, the compiler inserts both the function and the body of lambda into invocation place. You can find more about that in the official documentation.
Also, I added one method for each level, which sets level and message text at the same time. Here is an example for
The final step is writing a glue logic which creates an instance of
Log class, allows us to call its method to configure it and writes a message to logger implementations. Almost all the code in the snippet below relates to managing and calling logger implementations. Just keep in mind you need to register
LogWriter implementation. The only interesting function from the perspective of DSL is
log function creates a new instance of
Log class, invokes lambda against it and writes a configured record to loggers. As you can see, the function is higher-order as well, which means it will be used in DSL. Precisely, you need to call it as root of configuration passing lambda with invocation of configuring functions.
That’s all. Just a few classes and a couple of functions and you have a quite simple DSL for logging. This is an example of declarative language. In terms of log domain every block defines a state of the message and the whole config lambda defines the structure of the message to be logged. The order of blocks is not important here.
There is another kind of DSL – imperative. It means every block performs some action or command. And the whole “script” defines the sort of data-flow or control-flow. Testing may be a good candidate. Typically, in unit-testing a test case consists of 3 phases: mocks configuration, action and verification. So, we can define one root function
scenario and 3 functions for each phase:
review. Each of those functions should be defined with its own receiver and the receiver should have corresponding functions: various mocks configuration, different actions with a testable instance and assertions. The order of blocks will matter in this case.
This way you can implement plenty of DSLs, simple and complex. For example:
- definition of REST API (similar to ktor)
- definition of objects tree structure (like dictionary or database)
- any flow definitions (alternative to builder pattern for example)
- custom serialization format (reinventing protobuf)
- more and more
As you can see, it’s quite easy to write your own DSL in Kotlin. The power of Kotlin functions gives you an elegant way to make a declaration of whatever you want with minimal memory and call stack overheads. I hope this article clarified how to create custom DSL and why this is useful.
In the world of Kotlin there are several libraries which provide DSL. Here is a short list of them:
- Anko – set of libraries: common, layouts, sqlite, coroutines. Layouts is interesting to play with.
- Mockito-kotlin – beautiful DSL wrapping mockito
- Ktor – web-server written in Kotlin
- kotlinx.html – DSL to build HTML
- SeleniumBuilder – Selenium 2 wrapper
- Spring JPA – querying spring data JPA repositories using spring data Specifications
Also, I’ve created libraries with DSL itself, Android LogWriter implementation and bridge for slf4j logging system. The source code of library and sample application is available at github. I’d be grateful if you could add your ideas, bug reports and pull requests on the project page. To use the library, add to your