In the previous article we ended up with a functional DSL, yet incomplete since there’s still a very important detail left to cover. In this article, we are going to improve our DSL by restricting the implicit receivers’ scope within nested lambdas so the compiler will be able to point out errors or misuses.

Restriction within scopes

Lambdas, high-order functions and extension functions provide us flexibility when it comes to building objects. Although this is a good thing, it also opens the door to misuse and makes our DSL error prone. To mitigate this problem, it is necessary to restrict outer scopes inside nested lambdas.

What does this mean? I’ll illustrate this with an example, creating a Triangle inside a Square creation lambda which is inside another Square creation lambda:

You’ll agree with me that creating shapes this way doesn’t make sense and it shouldn’t be allowed except for a ComposedShape creation lambda. This is caused because when we have a lambda with a receiver inside another lambda with a receiver, the outer receiver can be accessed inside the inner lambda.

Kotlin provides us the @DslMarker annotation to create other annotations to restrict the access to receivers within lambdas. This annotation is applied to classes that define annotations.

— The @DslMarker annotation:

Classes that define annotations marked with the @DslMarker annotation are used to define DSLs. These annotations are used to mark classes and receivers, preventing receivers marked with the same annotation to be accessed inside one another.

In our DSL we need to invoke square, triangle and rhombus inside panel function lambda, but we need to restrict those invocations inside their own lambdas. In practice, all we need to do is restrict the implicit receiver of type Panel to be accessed inside square, triangle and rhombus lambdas. In order to achieve that, we need to mark those lambdas with the same annotation.

Let’s create a new annotation to define a DSL. Go to console_shapes_dsl.external package and add the following code to the Extension.kt file:

Now that we have defined a DSL, we can group lambdas to restrict their implicit receivers’ access. Let’s start with the panel function lambda and the square function lambda. Again, go to console_shapes_dsl.external package and open the Extension.kt file. Find the panel function and the square extension function and mark their receivers with the @ShapeDsl annotation as follows:

💬 Go to the main method and create a Triangle inside a Square lambda. Now you’ll see that the compiler throws an error, however, if you do it the other way around, creating a Square inside a Triangle lambda, the compiler doesn’t throw any errors. This is happening because we haven’t marked the triangle function lambda with the new annotation and the same applies to the rhombus function.

This is one approach to restrict access to implicit receivers. However, marking the class directly with the @ShapeDsl annotation is more appropriate.

💬 For the panel function case, we have no choice but doing it this way since we’re pretending that we don’t have access or permission to modify the initial code. If that wasn’t the case, we should mark the Panel class instead. On the other hand, we did create all shapes’ builders so we’ll mark those classes with the @ShapeDsl annotation.

Remove the @ShapeDsl annotation from the square extension function receiver:

Go to console_shapes_dsl.builders package and open the SquareBuilder.kt file. Mark the SquareBuilder class with the @ShapeDsl annotation:

💬 Again, you’ll see that it’s not possible to create a Triangle inside a Square lambda, but it is possible the other way around.

At this point we should also mark Triangle and Rhombus builders with the @ShapeDsl annotation, however, since all builders that require to be marked with this annotation extend from ShapeBuilder class, instead of marking each builder class, we should mark only ShapeBuilder class. This will affect all its subclasses and if we eventually add new builders, they will inherit this behavior directly.

Go to console_shapes_dsl.builders package and remove the @ShapeDsl annotation from SquareBuilder class:

Now add the @ShapeDsl annotation to ShapeBuilder class:

💬 Go to the main method and create a Triangle inside a Square lambda and the other way around. You’ll see that now the compiler throws an error in both cases. This also applies to Rhombus shapes.

If you wonder if we should restrict the access to outer implicit receivers inside composed function lambda, the answer is: No, we shouldn’t because a ComposedShape object is built by merging other shapes and those shapes should be allowed to be built directly inside its lambda.

I have only one thing left to tell you and that you should bear in mind. This restriction can be bypassed turning the implicit receiver into an explicit one. For example, if we wanted to build a Triangle inside a Square lambda, we can do it as follows:

Look at the this@panel reference. By referencing the outer receiver explicitly, we can build shapes inside other shapes’ lambdas.

In case you got confused with Extension.kt file modifications we have made so far, this is what it should look like:

Also, only the ShapeBuilder class should be marked with the @ShapeDsl annotation.

Finally, we have accomplished our initial goal. We managed to replace this code …

main.kt — Initial

…with our DSL syntax…

main.kt — Final

…without modifying a single line of the codebase, getting the exact same console output:

Console output

🔗 You can download the full project on my GitHub account at https://github.com/pencelab/Shapes-DSL. In the master branch you can find the codebase. In the dsl branch you can find the codebase and all the code that we wrote throughout these articles.

This is all you need to create a DSL in Kotlin. Although this subject is not difficult, it is a little complex at first sight, especially if we’re not familiar with features like high-order functions, lambdas and extension functions, or in general terms, without basic knowledge about functional programming.

There’s only one thing left to do: play and experiment a little bit with our DSL just to see what we can do. We will do that in the next and last article in which we will also draw some conclusions.

Continue with the next article Experimenting and conclusions

💬 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.