Writing Kotlin DSLs with nested builder pattern

Vince Delricco
ProAndroidDev
Published in
5 min readOct 11, 2018

DSLs can be an extremely useful way to create a declarative language for a specific use-case in your app. While attending KotlinConf 2018, I witnessed a lot of interest in DSLs. There were several talks on the subject featured in the main rooms.

It’s true that DSLs are a great way to add some readability to your code/configurations, but how do you write them to accept complex data structures in a clean, immutable way? In this article I hope to answer that question and explain each step along the way.

We’ll set up our premise with these data classes:

This is the data we want to represent, but what do we want our DSL to look like? It’s always important to have an idea of what you want to build before you begin building it. Here is what I’m envisioning:

Looks nice, huh? Now we just need to build it! Let’s dissect this DSL to see if we can back the logic out. The first thing you’ll notice is the entry point of the DSL, the business method. This method takes a lambda as a parameter as denoted by the { } notation. This may be tricky to see at first because the method doesn’t contain the traditional parens that surround the argument. When supplying a single lambda argument to a function, the parens can be omitted entirely (and actually should be omitted, according to the IDE warning Kotlin supplies that reads Lambda arguments should be moved out of parentheses), which gives us this really nice looking syntax.

So to start, the business method signature might look something like this:

This means that the method business takes a lambda that doesn’t explicitly return anything useful (Unit), but needs to spit out a Business object somehow. This may not make much sense at the moment so let’s do what good engineers do and break down the problem into a simpler problem. How about we just work with the Address class at first?

Again, here’s what the Address class looks like, and what the corresponding DSL should look like:

Notice for this example, I’ve removed the immutability of the Address class, and slightly changed the syntax of how we set the data. No worries, we will add both back in soon! So again, we have a method that looks like this:

But how can we return an Address object only given a lambda that returns Unit? The trick is to specify the receiver of the lambda expression! Kotlin allows this type of behavior by using the following syntax:

This method now accepts a lambda expression that will be run within the context of the Address class! This allows us access to the street and city variables we exposed in the Address class within the lambda that we supply to the address method (If this doesn’t make sense, I would recommend reviewing the Kotlin docs on this topic. This concept is the backbone of writing DSLs in Kotlin). Let’s implement the function:

All we have to do is create an empty Address object, apply the given lambda, which we have specified to have a receiver of type Address, and return it! We have built our first official Kotlin DSL. But of course we aren’t satisfied just yet. Let’s add back the things I removed for this simple example. Here is our goal:

This gives us the advantage of having an immutable Address class, and use the { } syntax instead of =. Again, we will start with our address method signature:

Obviously this will not work with our last implementation. We can’t just create an empty Address class and apply the lambda to set the values since it’s now immutable. We need another class to hold the data that gets set in the lambda, and then create and return the immutable Address at the end. Let’s use our trusty Builder pattern! It holds data, and is able to create an immutable class on demand. This would look something like this:

Now with our builder class created, let’s come back to our address method. Since we still want to use our street and city methods, it makes sense that we will want to pass a lambda with AddressBuilder as the receiver. Now, we can reimplement the function like this:

This new implementation allows us to use our DSL with the { } syntax, and preserve the immutability of the Address class! Exactly what we wanted. This is the basic pattern that allows us to build more complex data structures.

To finish our example, all we have to do is builderize all the things.

After studying the code, I think you’ll find that it’s really “builder’s all the way down”. The only tricky piece of code you’ll find is the convenience method called employees, which takes an EmployeeListBuilder-bounded lambda. We can abstract any piece of data with it’s own builder, so we’re just holding a list of Employee's in this builder.

Let’s tie it all together with the top level business method:

Beautiful, really. The only thing we have left to do is protect our data from potential misuse. If you’ve been following along in your IDE, you’ll notice a peculiar thing. Try doing something like this:

This is completely valid code within the DSL that we’ve defined, but this isn’t what we wanted! When defining an Address, we shouldn’t be allowed to define a name. Well why does this happen? The lambda that we pass to the address function is within the context of an AddressBuilder, which happens to be within the context of a BusinessBuilder, which does have a name method. This is the method that we’re accessing erroneously. To fix this, all we have to do is create what’s called a @DslMarker annotation:

And then annotate our builder classes with it:

You can read more about the @DslMarker annotation here. It was specifically created for this problem when creating DSLs. Now that we’ve protected our data from misuse, we’ve successfully implemented our original goal.

I hope you can tell just how extensible this type of pattern is. Your imagination truly is the limit when it comes to how you want to define your DSL. I think some of the most powerful use-cases lie within defining behavior as opposed to simple values as we’ve built in this tutorial. But that topic will wait for another day and another article.

Sign up to discover human stories that deepen your understanding of the world.

Membership

Read member-only stories

Support writers you read most

Earn money for your writing

Listen to audio narrations

Read offline with the Medium app

--

--

Published in ProAndroidDev

The latest posts from Android Professionals and Google Developer Experts.

Responses (6)

Write a response