Kotlin DSL | Coding a DSL: 6— The @DslMarker annotation
Restricting implicit receivers’ scope within lambdas
You can go to any article of this series by clicking on one of the links below:
Kotlin DSL
- Introduction
- Base knowledge to build a DSL in Kotlin — Part 1
- Base knowledge to build a DSL in Kotlin — Part 2
- Codebase: Project Shapes-DSL
- Coding a DSL: 1 — Package structure and the ‘Panel’ object
- Coding a DSL: 2 — The ‘Square’ object
- Coding a DSL: 3 — The ‘Triangle’ and ‘Rhombus’ objects
- Coding a DSL: 4 — The ‘Empty Space’ and the ‘Composed Shape’ object
- Coding a DSL: 5 — plus and minus operators and inline functions
- Coding a DSL: 6 — The @DslMarker annotation
- Experimenting and conclusions
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 thetriangle
function lambda with the new annotation and the same applies to therhombus
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 thePanel
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 …
…with our DSL syntax…
…without modifying a single line of the codebase, getting the exact same 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 thedsl
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
- 📖 You can learn about the @DslMarker annotation on Kotlin’s official documentation at https://kotlinlang.org/api/latest/jvm/stdlib/kotlin/-dsl-marker/