Kotlin DSL | Coding a DSL: 1 — Package structure and the ‘Panel’ object

Glenn Sandoval
Kotlin and Kotlin for Android
6 min readJun 28, 2021

Project organization, methodology and ‘Panel’ construction

You can go to any article of this series by clicking on one of the links below:

Kotlin DSL

  1. Introduction
  2. Base knowledge to build a DSL in Kotlin — Part 1
  3. Base knowledge to build a DSL in Kotlin — Part 2
  4. Codebase: Project Shapes-DSL
  5. Coding a DSL: 1 — Package structure and the ‘Panel’ object
  6. Coding a DSL: 2 — The ‘Square’ object
  7. Coding a DSL: 3 — The ‘Triangle’ and ‘Rhombus’ objects
  8. Coding a DSL: 4 — The ‘Empty Space’ and the ‘Composed Shape’ object
  9. Coding a DSL: 5 — plus and minus operators and inline functions
  10. Coding a DSL: 6 — The @DslMarker annotation
  11. Experimenting and conclusions

In the previous article I presented to you the codebase on which we’ll build a DSL.

🔗 If you haven’t downloaded the initial code yet, you can find it on my GitHub account:

In this article we’ll start building a DSL applying a methodology analogous to TDD.

As I mentioned at the beginning of this series, our goal is to start with code that looks like this…

Initial code

...to turn it into code that looks like this:

Final code

We will start to change the initial code sample, getting closer and closer until we get to the final code sample.

Package structure

Before start coding, we have to organize our project. Let’s create a new package at the same level of console_shapes, inside kotlin package and name it console_shapes_dsl. We are going to work exclusively inside this package, so everything outside this package will remain unmodified, except for the main method.

In order to create a DSL, we are going to apply the Builder design pattern and extension functions. To do so, create 2 new packages inside console_shapes_dsl and name them builders and external.

We are going to add all the different shapes’ builders inside the builders package. Inside the external package, we will add a file where we are going to place all the extension functions, high-order functions, helper functions or any other code that can’t be classified as part of a hierarchy.

💬 Although all the extension functions can be placed into their respective classes as member functions, we are going to stick with the extension functions approach as an exercise in case you eventually have to deal with code that already implements the builder pattern.

At this point, your project package structure should look like the following image:

Shapes-DSL: Package structure

Creating a ‘Panel’ object

The first thing we are going to do is slightly modify the main method changing the instructions

to

💬 The ellipsis indicates omission of the rest of the code which won’t be modified for the moment.

With this change, the code doesn’t compile, but don’t worry because that’s our intention. The idea is to write more code until it compiles so we can make another change that breaks the code again, and so on until we get to the final code.

At this point, our code should look like this:

main.kt —Step 1

Look carefully at the instruction panel { ... }. From this function we can deduce that it’s a high-order function that returns an instance of Panel. Additionally, we can clearly see that its last (and only) input parameter is a lambda — trailing lambda — .

The only thing left to deduce is its lambda function type. In order to make the code of the lambda work, there must be a variable named panel of type Panel which implies that it’s passed in as a parameter to the lambda. Furthermore, since the last line of the lambda is a function call that doesn’t return a value, we can conclude that the lambda’s return value is of type Unit. The lambda function type then corresponds to (Panel) -> Unit. The signature of the function panel would be as follows:

fun panel(init: (Panel) -> Unit): Panel

💬 I named the lambda init since its code block creates a Panel and its shapes.

Now that we have deduced the signature of the function panel, it’s time to write its code. Create a new Kotlin file named Extension.kt inside console_shapes_dsl.external and paste what we have so far:

fun panel(init: (Panel) -> Unit): Panel {

}

Both, its lambda input parameter and its return value are Panel objects. This is an indication that both objects might be the same one. Its code would look like this:

fun panel(init: (Panel) -> Unit): Panel {
val panel = Panel()
init(panel)
return panel
}

However, we can take advantage of the apply function to reduce its code to a single line:

fun panel(init: (Panel) -> Unit): Panel {
return Panel().apply { init(this) }
}

Find the main method and add all the necessary imports to its file:

import console_shapes_dsl.external.panel

You’ll see that every reference to the panel object generates a compilation error and this is not only because there’s no longer a variable named panel, but because there is a function named panel which make things even “more confusing” for the compiler. To fix this just name its input parameter as panel using the arrow notation:

panel { panel ->
...
}

▶️ If you run the code, you’ll see that it works again.

Naming a parameter panel when there is already a function named panel makes our code unclear and confusing, so we should change its name or just remove the arrow notation and use its default name it instead:

panel {
...
it.addShape(square)
...
}

Now using its default name it is fine but not fine enough. It would be better if we could use an implicit reference so we could omit it when calling all Panel’s member and extension functions. This leads us to having a lambda whose scope corresponds to an object of type Panel.

To accomplish this, we have to turn the lambda into a lambda with a receiver of type Panel. Let’s move its input parameter out of the parentheses to make it its receiver:

fun panel(init: Panel.() -> Unit): Panel

Now its lambda can only be invoked by a Panel object which now, instead of being passed in as parameter, becomes its receiver:

fun panel(init: Panel.() -> Unit): Panel {
val panel = Panel()
panel.init()
return panel
}

Let’s reduce its code to a single line with the apply function:

Now replace all the panel references (or it references) with the implicit receiver this inside the lambda of the panel function invocation, located in the main method:

panel {
...
this.addShape(square)
...
}

Or even better, just remove all panel references (or it references). This results in a more readable code. After these modifications, our main method’s content should look like this:

main.kt — Step 2

▶️ If you run the code, you’ll see that it works again.

I will finish this article at this point, since it’s important for you to have everything clear before moving forward. If there’s something not so clear for you, I suggest you to read it carefully again. What we are going to do in the next article is quite similar to what we did in this one.

Continue with the next article Coding a DSL: 2 — The ‘Square’ object

💬 If you enjoyed this article, you can show your appreciation by buying me a coffee at the link below. Thanks for reading and for your support.

--

--

Glenn Sandoval
Kotlin and Kotlin for Android

I’m a software developer who loves learning and making new things all the time. I especially like mobile technology.