Kotlin DSL

A domain-specific language (DSL) is a computer language specialized to a particular application domain. This is in contrast to a general-purpose language (GPL), which is broadly applicable across domains. There are a wide variety of DSLs, ranging from widely used languages for common domains, such as HTML for web pages, down to languages used by only one or a few pieces of software.

Kotlin DSL

Kotlin provides first class support for DSL which allows us to express domain-specific operations much more concisely than an equivalent piece of code in a general-purpose language.

Let’s try and build a simple DSL in Kotlin –

This should be familiar to people using gradle as their build tool. Above DSL specifies compile and testCompile dependencies for a gradle project in very concise and expressive form.

How does Kotlin support DSL

Before we get in to Kotlin’s support for DSL, let’s look at lambdas in Kotlin.

buildString() takes a lambda as a parameter (called action) and invokes it by passing an instance of StringBuilder. Any client code which invokes buildString() will look like the below code –

A few things to note here –

  • buildString() takes lambda as the last parameter. If a function takes lambda as the last parameter, Kotlin allows you to invoke the function using braces { .. }, no need of using parentheses
  • it is the implicit parameter available in lambda body which is an instance of StringBuilder in this example

This information is good enough to write a gradle dependencies DSL.

First Attempt at DSL

In order to build a gradle dependencies DSL we need a function called dependencies which should take a lambda of type T as a parameter where T provides compile and testCompile functions.

Let’s try –

dependencies is a simple function which takes a lambda accepting an instance of DependencyHandler as an parameter and returning Unit. DependencyHandler is the type T which has compile and testCompile functions.

Client code for the above concept will look like –

Are we done? Not really. The problem is the implicit parameter it used in the client code. Can we remove it? In order to remove implicit parameter, we need to look at “Lambda With Receiver”.

Lambda With Receiver

Receiver is a simple type in Kotlin which is extended.

Let’s see this with an example –

We have extended String to have lastChar() as a function which means we can always invoke it as –

Here, String is the receiver type and this used in the body of lastChar() is the receiver object. Can we combine these 2 concepts – lambda and receiver?

Let’s rewrite our buildString function using lambda with receiver –

A few things to note here –

  • buildString() takes a lambda with receiver as an parameter
  • StringBuilder is the receiver type in the lambda
  • the way we invoke action function is different this time. Because action is an extension function on StringBuilder we invoke it using sb.action(), where sb is an instance of StringBuilder

Let’s create a client of buildString function –

Isn’t this brilliant? Client code will always have access to this while invoking a function which takes lambda with receiver as a parameter.

Shall we rewrite our gradle dependencies DSL code?

Another Attempt at DSL

The only change we have done here is in dependencies function which takes a lambda with receiver as the parameterDependencyHandler is the receiver type in action parameter which means the client code will always have access to the instance of DependencyHandler.

Let’s see the client code –

We are  able to create a DSL using Lambda with Receiver as a parameter to a function.

Operator Function invoke()

Kotlin provides an interesting function called invoke which is an operator function. Specifying invoke operator on a class allows it to be called on any instances of the class without a method name.

Let’s see this in action –

A few things to note about invoke() here –

  • is an operator function
  • takes parameter
  • can be overloaded
  • is being called on the instance of Greeter class without method name

Let’s use invoke in building DSL –

Building DSL using invoke function

We have defined an operator function in DependencyHandler which takes a lambda with receiver as a parameter. This means invoke will automatically be called on instance(s) of DependencyHandler and client code will have access to instance of DependencyHandler.

Let’s write the client code –

invoke() can come in handy while building DSL.

Conclusion

  • Kotlin provides a first class support for DSL which is type safe
  • One can create a DSL in Kotlin using –
    • Lambda as function parameters
    • Lambda With Receiver as function parameter
    • operator function invoke along with Lambda With Receiver as function parameter

References

  • Kotlin In Action

You may also like...

Leave a Reply

avatar
  Subscribe  
Notify of