Kotlin DSL | Coding a DSL: 4 — The ‘Empty Space’ and the ‘Composed Shape’ object
Adding empty spaces between shapes and merging shapes to build composed shapes
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, Triangle and Rhombus shapes’ builders were created. Also, we created the extension functions triangle
and rhombus
which build shapes (using their corresponding builders) to add them to the Panel. We ended up making a little refactoring to abstract all builders’ common behavior to a parent class. At the end of the previous article, the main
method’s content looked like this:
In this article we will change the way we add “empty spaces” and ComposedShapes to the Panel.
The ‘space’ function to add empty spaces
Our next goal is to change all addShape(Space)
instructions to space()
instructions. The main
method’s content should look like this:
Clearly, space()
is a function invocation. We can deduce that addShape(Space)
is executed inside this function to add the Space
object to the Panel.
💬 Remember that
addShape(Space)
is called through the implicit receiverthis
.
The space
function is not that different from square
, triangle
and rhombus
functions since the Space object extends from Shape
class. We can tell that in order to invoke addShape
inside the space
function, we need to pass in a Panel reference. We can conclude that the space
function is also a Panel
’s extension function just like the other 3 functions. Its signature should look like this:
fun Panel.space()
Go to console_shapes_dsl.external
package and add the following code to the Extension.kt
file:
fun Panel.space() {
}
The only thing that it has to do is add a Space
object to the Panel. Its code should look as follows:
▶️ If you run the code, you’ll see that it works again.
Building the ‘ComposedShape’ object
Out next goal is to replace all instructions like addShape(ComposedShape(shapeX, shapeY, ComposedShape.Operation.XYZ))
with instructions like composed { shapeX xyz shapeY }
, where shapeX
and shapeY
represent instances of Square
, Triangle
, Rhombus
or ComposedShape
classes and Operation.XYZ
or xyz
correspond to Operation.UNION
or Operation.INTERSECTION
operations.
We can separate these instructions in 2 parts:
- Replacement of the instruction
ComposedShape(shapeX, shapeY, ComposedShape.Operation.XYZ)
with the instructionshapeX xyz shapeY
. - Replacement of the invocation
addShape
with thecomposed
invocation.
In addition, both parts must be interchangeable, that is, expresions like composed { ComposedShape(shapeX, shapeY, ComposedShape.Operation.XYZ) }
or addShape(shapeX xyz shapeY)
should be allowed.
Given these equivalences we can say that an instruction like shapeX xyz shapeY
creates an object of type ComposedShape
and an invocation to the function composed
adds a Shape of type ComposedShape
to the Panel.
🔗 If an instruction like
shapeX xyz shapeY
seems strange to you, take a look at the section Infix notation in one of my previous articles.
— union
and intersection
operations in infix notation
We are going to start with instructions ComposedShape(shapeX, shapeY, ComposedShape.Operation.XYZ)
. Our goal is to replace them with instructions shapeX xyz shapeY
. Change the main
method’s content to the following:
These new functions and their respective tasks are:
- Function
union
merges 2 objects of typeShape
and returns a new object of typeComposedShape
whose parameteroperation
corresponds toOperation.UNION
. - Function
intersection
merges 2 objects of typeShape
and returns a new object of typeComposedShape
whose parameteroperation
corresponds toOperation.INTERSECTION
.
Since both functions must be able to be invoked in infix notation, they must be marked with the infix
modifier. In order to merge 2 objects of type Shape
, these functions have to be extension functions of Shape
and they have to receive one Shape
as input parameter. Their signatures are as follows:
infix fun Shape.union(shape: Shape): ComposedShape
infix fun Shape.intersection(shape: Shape): ComposedShape
Go to console_shapes_dsl.external
package and add the following code to the Extension.kt
file:
infix fun Shape.union(shape: Shape): ComposedShape {
}infix fun Shape.intersection(shape: Shape): ComposedShape {
}
Function union
has to return an object of type ComposedShape
so it has to create it first passing in both Shapes and the value Operation.UNION
to its constructor. It should look like this:
Similarly, function intersection
does the same thing but passing in the value Operation.UNION
instead. It should look like this:
▶️ If you run the code, you’ll see that it works again.
— Adding a ComposedShape
with the function composed
Now that we can create Composed Shapes in infix notation, our next goal is to change addShape
invocations to composed
invocations whose only task consists of adding a shape of type ComposedShape
to the Panel. The main
method’s content should look like this:
Let’s analyze the function composed
:
- Its code block implies that it receives a lambda as input parameter, therefore, it is a high-order function.
- The last instruction in every single lambda is a
union
orintersection
invocation, therefore, its lambda returns an object of typeComposedShape
. - No lambda is expressed in arrow notation, nor they use the default reference
it
, therefore, its lambda doesn’t have input parameters. - Inside its lambda we can find
union
orintersection
invocations which are extension functions of thePanel
class, therefore, its lambda is scoped to an object of typePanel
, that is, it’s a lambda with a receiver of typePanel
.
The only thing that we can’t deduce from the code we want to get to is if the function composed
returns something or not. Apparently, it doesn’t since there are no assignments to variables. However, the function composed
is at the same level of the functions square
, triangle
and rhombus
and its essence is practically the same — they all create a shape and add it to the Panel — . We could assume that it returns the object of type ComposedShape
that it adds to the Panel. This would let us hold its reference to use it afterwards to merge it with other shapes and create even more complex shapes.
After this analysis, its signature should look as follows:
fun Panel.composed(init: Panel.() -> ComposedShape): ComposedShape
💬 I named its lambda
init
since its code block creates and returns a shape of typeComposedShape
.
Go to console_shapes_dsl.external
package and add the following code to the Extension.kt
file:
fun Panel.composed(init: Panel.() -> ComposedShape): ComposedShape {
}
Before we write its code, let’s enumerate what it has to do:
- Run its lambda.
- Add to the Panel the object returned by its lambda.
- Return the same object that was added to the Panel.
Its code should look as follows:
fun Panel.composed(init: Panel.() -> ComposedShape): ComposedShape {
val shape = init()
this.addShape(shape)
return shape
}
And as we have done before, we can reduce it to a single line with the scope function also
:
▶️ If you run the code, you’ll see that it works again.
We’re done for now. You can play around with this new code and try creating Composed Shapes combining invocations to addShape
passing in Composed Shapes expressed in infix notation or the other way around, you can try passing in a regular Composed Shapes instantiation to the composed
function:
We are very close to finish, all we have left to do is overload +
and -
operators to merge shapes with an arithmetic-like notation, but that is what we are going to do in the next article, plus a little optimization.
Continue with the next article Coding a DSL: 5 — plus and minus operators and inline functions
- 📖 You can learn about High-Order Functions on Kotlin’s official documentation at https://kotlinlang.org/docs/lambdas.html
- 📖 You can learn about Lambdas with Receivers on Kotlin’s official documentation at https://kotlinlang.org/docs/lambdas.html#function-literals-with-receiver
- 📖 You can learn about Scope Functions on Kotlin’s official documentation at https://kotlinlang.org/docs/scope-functions.html