Expand my Community achievements bar.

Building Elegant DSLs with Kotlin




Authors: Rares Vlasceanu, Florentin Simion, and Jenny Medeiros

Banner image.png

This is the second of a three-part series in which we re-introduce developers to Kotlin as an alternative for building server-side applications. In this post, we focus on the advantages of Kotlin to build efficient DSLs in Adobe Experience Platform. Read part one here.

Domain-specific languages (DSLs) have long been used to solve specific problems in markup, modeling, and even programming languages. While they are known for their swift implementation and readable syntax, they can also come at a high cost and even limitations, especially when employed in a narrow domain. However, we believe that with the right tools, building DSLs can be both easy and efficient.

For context, there are two main types of DSLs:

  • External DSL: this is a language parsed independently of the general-purpose language (e.g. Java). Creating such a DSL is an intricate task, but has the advantage of not being limited to the host language capabilities, which could also influence their form and/or expressivity.

But what if we could build an internal DSL with the look and feel of a well-designed external DSL and the advantages of a fluent interface — just without the complexity?

In this post, we outline why we prefer Kotlin when building efficient DSLs, accompanied by a step-by-step guide on how to build one.

Simplifying DSLs with Kotlin

Kotlin is a modern language known for its readable, clean, and concise syntax. With its advanced functional programming capabilities, we can create type-safe, statically typed builders that act as DSLs, which are suitable for expressing complex hierarchical data structures in a semi-declarative way.

For example, take the problem of building JSON payloads for black-box testing RESTful applications. The following figures show two very distinct options for doing this:

Figure 1: JSON payload using string concatenationFigure 1: JSON payload using string concatenation

Figure 2: JSON payload using a DSLFigure 2: JSON payload using a DSL

The first figure shows a typical script in which we concatenate strings to create the JSON payload, whereas in the second we have the same script but using a Kotlin-built DSL. The latter is clearly much easier to understand than its counterpart. Moreover, the DSL variant benefits from compile-time syntax and type check, guaranteeing a JSON structure that is syntactically correct.

To showcase the features that make Kotlin our preferred option for building DSLs, we will walk through the second script in our example to describe its building blocks and how they are implemented in Kotlin.

We will begin by defining the model we want to build. In this case, a User with one or more Phone Number entries. For this, we need at least two classes, one to represent each of the aforementioned entities.

User class

For reference, here is the DSL we are aiming to build:

Figure 3: A new user build with the Kotlin DSLFigure 3: A new user build with the Kotlin DSL

For the component handling the User entity, we have a class named UserBuilder, which helps describe the root JSON object and defines properties like nameemail, phoneNumbers (list).

Figure 4: The UserBuilder classFigure 4: The UserBuilder class

One thing to notice in the above builder class is the content property: it is initialized with a new instance of a JSON object node. To represent JSON objects, we are using the popular Jackson library, although any other alternative would work just as well.

As shown in our reference DSL, to start building the structure we want to state the following:

Figure 5: Example script in Kotlin of User DSL usageFigure 5: Example script in Kotlin of User DSL usage

This is just plain Kotlin functional programming, where the user is a function call that takes a lambda expression as an argument. This is defined as follows:

Figure 6: The User builder functionFigure 6: The User builder function

As shown above, this function takes a parameter named init, which is itself a function — a lambda with receiver. This type of lambda combines the properties of extension functions and lambda expressions by adopting the lambda function type declaration with the addition of a receiver through the dot notation:

Receiver.(parameter1, …, parameterN) -> returnType

In our case, init has the type UserBuilder.() -> Unit, which means that we need to pass an instance of type UserBuilder (a receiver) when invoking the function. This allows us to refer the receiver using the “this” keyword and access all its public members within the function.

Figure 7: Accessing the members of the UserBuilder classFigure 7: Accessing the members of the UserBuilder class

The invocation above resembles our final DSL, but it is still unnecessarily cluttered. This is where Kotlin’s brevity comes into play, allowing you to omit:

  • The parentheses when invoking the user function and simply pass the lambda as a block, since it is the last argument of our function.

This brings us to the cleaner-looking code shown below:

Figure 8: Syntax brevity in User builder’s init functionFigure 8: Syntax brevity in User builder’s init function

For added clarity, this code snippet does the following:

  • Creates a new instance of UserBuilder.

Phone Number class

Now we can tackle the User’s phone numbers. Since one user may have multiple phone numbers, we will first define a DSL for building PhoneNumber entries:

Figure 9: The phoneNumber builder function and its associated classFigure 9: The phoneNumber builder function and its associated class

For brevity, we will only set the actual phone number in our entries.

Next, we need to hook it into our main user builder. As you might expect, phoneNumbers in our DSL is nothing more than a builder function invocation:

Figure 10: Adding the phoneNumbers builder functionFigure 10: Adding the phoneNumbers builder function

The snippet above expects a JSON array node to be produced by the PhoneNumbersBuilder when initialized with the provided init function. This JSON array node is then simply added as a property of our User JSON object.

But what do we put in this init function? One convention in Kotlin builders/DSL is to use the “+” sign when adding items to a collection, like so:

Figure 11: Adding PhoneNumber entries to the collectionFigure 11: Adding PhoneNumber entries to the collection

These are function calls that invoke a prefix unaryPlus() operation. Those operations are defined by an extension function unaryPlus() on String (the first one) and on ObjectNode (the result of a phoneNumber builder):

Figure 12: Initializer functions for PhoneNumber entriesFigure 12: Initializer functions for PhoneNumber entries

Using the unaryPlus operator is a matter of preference. You can very well have a DSL that uses a standard method invocation instead. However, when used, the unaryPlus operator allows for a clearer syntax and better code indentation in your builder.

Now we have our two main classes and a complete DSL, that we will show below:

Figure 13: The full User DSLFigure 13: The full User DSL

As you can see, the DSL builder has more than a few lines of code even for our simple model, but it pays off in the long run for its efficiency and clean, readable syntax.

What’s next in the Kotlin series

In this post, we learned how to build a simple DSL for creating JSON payloads. Although Kotlin also has other, more powerful features when it comes to DSL creation, such as scope control markers that can limit the functions invoked in a certain context to prevent invalid object creations.

Because of worthwhile features like these, many tools have chosen or migrated to Kotlin for building their DSLs. One such tool is Gradle, which is a build tool that migrated their DSL from Groovy to Kotlin. Another good example is the familiar Java framework Spring Boot, which created KoFu as a microframework based on Kotlin that makes it easy to create Spring applications with functional APIs instead of annotations.

In our next post, we will focus on another of Kotlin’s main advantages for streamlined development: asynchronous programming with coroutines.

Follow the Adobe Experience Platform Community Blog for more developer stories and resources, and check out Adobe Developers on Twitter for the latest news and developer products. Sign up here for future Adobe Experience Platform Meetups.

Additional resources

Originally published: Jul 9, 2020