Writing Kotlin DSLs with nested builder pattern

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.