Kotlin DSL | Base knowledge to build a DSL in Kotlin — Part 2
Kotlin features that help us write more idiomatic DSLs
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
This is the second part of Base knowledge to build a DSL in Kotlin. In the first part, I covered all the Kotlin features that you must know to build a DSL. These features are:
- Lambdas
- Higher-order functions
- Extension functions
- Lambda with receiver
- Scope functions
- Builder design pattern
If you’re not familiar with any of these features, I suggest you to take a look at it before moving forward. In this second part, I’ll show you Kotlin features that allow us to write a more idiomatic DSL. These features are:
Infix notation
If you have experience in Kotlin, you probably have defined a Map
collection with the key to value
notation or maybe you have defined ranges and for
loops like this for (i in 1 until 100 step 2)
. The words to
, until
and step
are infix functions.
A function invocation in infix notation omits its parentheses and the dot. For example, a regular function call like a.until(b)
can be written in infix notation like this a until b
. To make a function able to be written in infix notation, all you have to do is mark it with the infix
modifier:
In the previous example, the extension function remove
is marked with the infix
modifier. This allows us to invoke it through any String in either regular notation — Line 7 — or infix notation — Line 8 — . Basically, all this function does is remove the character passed in as input parameter in regular notation, or declared to the right side of the function name in infix notation.
The previous code outputs the following:
Ideally, infix notation is applied to let us write more idiomatic code. By using infix notation, our goal should be making our code seem like it was written in natural language. In order to accomplish that, we should not only use infix notation, but choosing meaningful and accurate names for our functions and variables. To illustrate this, take a look at the next example:
In this example, I defined a class named Car
with a property named speed
which corresponds to the speed the car moves at. Also, I defined an enum class named Direction
with the 4 main cardinal directions. The idea is to make a Car
object move through x-axis (EAST and WEST) and y-axis (NORTH and SOUTH) by invoking function driveTo
which is marked with the modifier infix
. Its property coordinates
indicates where the car is at any moment. Lines 25, 27, 29, and 31 are expressed in infix notation.
The previous code outputs the following:
Finally, to define infix functions, you must take into account the following restrictions:
- It must be either an extension function or a member function.
- It must have one input parameter.
- Its input parameter must not be marked with the
vararg
modifier and it cannot have a default value.
🔗 If you want to learn more about infix notation, take a look at the section Infix notation of Kotlin’s official documentation at https://kotlinlang.org/docs/functions.html#infix-notation
Operator overloading
In programming, operator overloading is a way to define a function with a reserved/special name according to the operator to be overloaded. Some operators are +
, -
, *
, /
, %
, +=
, <
, >
, etc. When an operator is overloaded, we can use it to invoke its corresponding function.
In Kotlin, in order to overload an operator, a function has to be created and marked with the operator
modifier. Since each operator has a reserved name, this function has to be named according to the operator to be overloaded and it has to be either a member function or an extension function.
Let’s see an example with the modulus or remainder operator %
:
To overload the %
operator, it is necessary to create a function and name it rem
. In this case it’s an extension function with a receiver of type String
which enables us to apply it on any text — line 4 — . It’s also possible to invoke the function as usual — line 6 — .
All rem
function does is return true
if the String length is a multiple of the number passed in as input parameter. Although I could have done anything I want and return whatever I want, the idea is to use our common sense and implement functions according to the operator in question. That is, for instance if I wanted to overload the +
operator and apply it on 2 objects of type X, we would expect the result to be another object of type X whose properties are additions of their corresponding properties.
To illustrate this, take a look at the next code snippet where a class named LivingBeing
is created with 2 properties: developmentCycle
and sex
. The main idea is to overload the +
operator to combine 2 LivingBeing
objects which will result in a new LivingBeing
object.
Clearly my intention was to simulate the reproduction of living beings. When the +
operator is applied on 2 objects of type LivingBeing
, a new LivingBeing
object is returned, but that’s not all. In order to “reproduce”, their property sex
must be different from each other’s and their property developmentCycle
must be at least equal to MIN_DEV_CYCLE_TO_REPRODUCE
. Finally, if both conditions are met, a new LivingBeing
object is created with its property developmentCycle
set to 0 and its property sex
randomly set.
As you can see, I overloaded the +
operator to make an operation analogous to an addition. That is, when 2 objects of type LivingBeing
are “added”, the result is a new LivingBeing
object. However, overloading the +
operator doesn’t necessarily mean that the result has to be a new object. For instance, if we added 2 colors expressed in hexadecimal notation, we could overload the +
operator to return the result of adding both numbers: Red + Blue = Magenta (#FF0000 + #0000FF = #FF00FF
).
Make sure to apply common sense every time you overload an operator and, just like your variables and functions have meaningful and descriptive names, you overload it according to its meaning.
Now that you know how to overload an operator, you may now practice with the rest. Here’s an image with all operators that can be overloaded in Kotlin and their respective reserved/special function names:
🔗 If you want to learn more about this topic, take a look at the section Operator overloading of Kotlin’s official documentation at https://kotlinlang.org/docs/operator-overloading.html
Inline functions
When we create a high-order function, the compiler turns its function types into instances of Function#
interfaces. If it gets invoked many times, there would be an excessive and unnecessary use of resources because of constant allocations and functions invocations. To solve this issue, Kotlin provides us with the inline
modifier.
By marking a function with the inline
modifier, we can achieve a more efficient performance since the compiler will replace every invocation with its implementation. Furthermore, all of its function types will no longer be converted into objects, but they will become inline too.
Let’s illustrate it with a simple example:
The main
function invokes executeBetweenPrints
which prints out the phrase "First print"
to the console, then it executes task: () -> Unit
lambda and it finishes printing out the phrase "Last print"
. All task
lambda does is print out "My task is executing..."
to the console.
The previous code outputs the following:
If you compile and decompile the bytecode to Java code, you would get code similar to the following (although with much more noise):
Take a look at the function executeBetweenPrints
. It receives an instance of Function0
interface which wraps the lambda’s implementation inside its invoke
method.
If executeBetweenPrints
was invoked just a few times, then marking it with the inline
modifier wouldn’t make any difference in its general performance. However, if it was invoked repeatedly inside a loop or inside of a stream operator, each invocation would represent a new memory allocation and this would lead to an excessive use of resources.
Now let’s mark the function executeBetweenPrints
with the inline
modifier to see the difference:
If we compile it and decompile the bytecode to Java code, we’d get the following code:
The executeBetweenPrints
invocation has been replaced with its implementation, avoiding memory allocations with anonymous classes.
At this point I guess you’re considering to mark every function with the inline
modifier. Not so fast! As I mentioned before, if an inline
function is invoked just a few times, there won’t be a significant performance improvement and it would be actually counterproductive. You should know that if an inline function is extensive, the generated bytecode will be too large. In general terms, inline functions larger than 4 lines are not recommended.
To have a better general idea about when it is convenient to mark a function with the inline
modifier, you can take into account the following indicators:
— It is a high-order function.
— It is invoked repeatedly.
— It’s not larger than 4 lines of code.
If a function meets all those indicators, consider marking it as inline
.
Finally, you should know that sometimes it’s not possible to mark a function with the inline
modifier because of some restrictions, mainly because of the generated code, but that can be solved with modifiers like noinline
, crossinline
and reified
; although it depends on each use case. Since we won’t need any of those modifiers, they are out of the scope of this series.
🔗 If you want to learn more about this topic, take a look at the section Inline functions of Kotlin’s official documentation at https://kotlinlang.org/docs/operator-overloading.html
🔗 You can also check out this video where Florina Muntenescu explains the
inline
modifier: https://www.youtube.com/watch?v=wAQCs8-a6mg
Singleton design pattern/antipattern
One of the most controversial design patterns is the Singleton pattern. I call it pattern/antipattern because in our community there are developers who consider that this pattern generates more harm than good.
A Singleton is just an object that can be instantiated only once. That is, there will be only 1 instance of that object during the application lifecycle. This is possibly the simplest design pattern and while it can be very powerful, it can certainly also be very damaging. To avoid misuse, this design pattern is usually applied correctly under circumstances such as the following:
— When a single access point to some source of information, such as a database or a web service, is required.
— When an object creation is very expensive and the system requires a more dynamic performance. This is usually very common in videogames development.
— When an object that never changes works as a special “message” or as an object of a type defined within a set of objects of the same class/category. This is usually common when the Observer design pattern is applied and different “messages” of the same class are required, commonly defined under a sealed class.
There might be other use cases and each of them should be considered carefully, but in general terms, this design pattern tends to coincide from project to project. After all, it’s a pattern for a reason.
Creating a Singleton in Kotlin is very similar to creating a class, with the only difference that instead of the class
keyword, you should use the object
keyword:
With the object
keyword, a class and its instance is created at the same time. A Singleton is created lazily, that is, until it’s accessed for the first time. In order to access it, you have to call it by its name directly.
Just like a regular class, an object can inherit from a class and it can also implement interfaces:
🔗 If you want to learn more about this topic, take a look at the section Object expressions and declarations of Kotlin’s official documentation at https://kotlinlang.org/docs/object-declarations.html
We’re done with all the Kotlin features that allow us to write more idiomatic DSLs. In the next article I’ll present you the codebase before we start building a DSL on top of it.
Continue with the next article Codebase: Project Shapes-DSL
💬 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.
- 📖 You can read more about Infix Notation on Kotlin’s official documentation at https://kotlinlang.org/docs/functions.html#infix-notation
- 📖 You can read more about Operator Overloading on Kotlin’s official documentation at https://kotlinlang.org/docs/operator-overloading.html
- 📖 You can read more about Inline Functions on Kotlin’s official documentation at https://kotlinlang.org/docs/inline-functions.html
- 📖 You can read more about Object Declarations on Kotlin’s official documentation at https://kotlinlang.org/docs/object-declarations.html