Kotlin DSL | Coding a DSL: 4 — The ‘Empty Space’ and the ‘Composed Shape’ object

Glenn Sandoval
Kotlin and Kotlin for Android
6 min readJul 8, 2021

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

  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, 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:

main.kt — Step 5

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:

main.kt — Step 6

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 receiver this.

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 instruction shapeX xyz shapeY.
  • Replacement of the invocation addShape with the composed 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:

main.kt — Step 7

These new functions and their respective tasks are:

  • Function union merges 2 objects of type Shape and returns a new object of type ComposedShape whose parameter operation corresponds to Operation.UNION.
  • Function intersection merges 2 objects of type Shape and returns a new object of type ComposedShape whose parameter operation corresponds to Operation.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:

main.kt — Step 8

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 or intersection invocation, therefore, its lambda returns an object of type ComposedShape.
  • 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 or intersection invocations which are extension functions of the Panel class, therefore, its lambda is scoped to an object of type Panel, that is, it’s a lambda with a receiver of type Panel.

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 type ComposedShape.

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:

  1. Run its lambda.
  2. Add to the Panel the object returned by its lambda.
  3. 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.

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