I’ll split this section in 2 parts. In the first part, I will cover Kotlin features that you must know to build a DSL. These features are:

In the second part, I will cover Kotlin features that help us write a more idiomatic DSL. These features are:

If you’re not familiar with any of these Kotlin features, consider taking a look at them before moving forward.

Lambdas

Lambda is the eleventh letter of the Greek alphabet. The symbol used to represent this letter is the following: Λ or λ. This is the most important piece when it comes to functional programming because it is a representation of the processes of functional calculus in mathematics where this paradigm comes from.

A lambda, in general terms, defines an algorithm, a behavior, an action, a procedure, etc. You can call it whatever you want, however, if you look at it closely, a lambda is just the body of a function, therefore it follows the same structure. In short, a lambda is the definition of a process that returns a result given some specific inputs.

A lambda in Kotlin is declared with a code block enclosed in curly braces with the possibility of being stored in a variable:

As mentioned before, a lambda follows the same structure of a function: input parameters and a return value. The previous example corresponds to a lambda whose function type is () -> Unit. That is a function that doesn’t receive input parameters and doesn’t return any value. You must bear in mind that the return value of a lambda always corresponds to the last line or last instruction of the code block.

Since Kotlin infers the type of a variable, we could omit its function type in the previous example, however, if we wanted to be explicit, we must do it like this:

Take a look at the first line, specifically at the part that corresponds to the function type () -> Unit. All input parameters’ data types must be declared between parenthesis if any. If there is none, then that lambda doesn’t receive input parameters. Its return type must be declared next to the arrow, and as you know, Unit data type corresponds to the default data type returned by any function that doesn’t specify its return type.

If we needed to define a lambda that receives an integer as an input parameter, and returns its squared value, we must declare its data type like so (Int) -> Int. The entire code would look like this:

The code block enclosed in curly braces matches with the data type. This code block is expressed in the arrow notation. What’s to the left side of the arrow corresponds to its input parameters. What’s to the right side of the arrow correspond to its algorithm or procedure which uses its input parameters. Remember that the last instruction corresponds to its return value, x * x in this case.

In Kotlin, if a lambda has only one input parameter, the arrow notation can be omitted. In such case, its input parameter will take the name it by default.

To execute the code of a lambda, we just have to call it by its variable name adding parentheses. If the lambda receives input parameters, they must be enclosed in those parentheses.

Finally, I will show you a slightly more elaborated example so you can observe the declaration of a lambda that receives more than one input parameter and that requires a slightly more complex algorithm to come up with a return value.

The lambda declared previously will count the number of times that a specific number appears within a list of numbers. In order to do that, two input parameters are required. The first parameter corresponds to the list of numbers. The second parameter corresponds to the number to be found. When more than one input parameter is received, their data types must be separated by commas. In this case, the function type is (List<Int>, Int) -> Int.

The only thing I have left to say to you is use this “power” with responsibility since you can now define lambdas as complex as you want, like lambdas that receive lambdas that return lambdas. Keep your code clean and easy to read. Have in mind that other developers or yourself in the future will have to maintain that code and after a while you won’t remember exactly what it does.

🔗 If you want to learn more about this topic, take a look at the section High-order functions and lambdas of Kotlin’s official documentation at https://kotlinlang.org/docs/lambdas.html

High-order functions

In Kotlin, functions are first class citizens. That means that functions can be passed in as parameters and can be returned by other functions.

In the previous section I showed you what a lambda is and its possibility of being stored in a variable. If you think through, you can do with a lambda everything you do with any other data type, like overriding the variable where it is stored, passing it in as an input, receiving it as a return value from a function, adding it to a collection, etc.

A high-order function looks like this:

All printAndExecuteTask function does is print out to the console the String received as first parameter named text and then execute the function received as second parameter named task. This second parameter corresponds to a function type () -> Unit and it is what makes printAndExecuteTask function be a high-order function.

As you know, when invoking any function you must match the order of input parameters and their data types. You also must match lambdas and function types when passing them in as parameters. For example, if the function type corresponds to () -> Unit then it won’t be possible to pass a lambda in whose function type is (Int) -> Unit or any other function type different from () -> Unit.

The previous function invocation has to be done like this:

From line 1 to line 3, a lambda whose function type is () -> Unit is declared, and it matches with the second input parameter of the function printAndExecuteTask. On line 4, the printAndExecuteTask function invocation is done.

When we execute the previous code, we get the following output:

Let’s see a little bit more elaborated example. This time we’ll have a high-order function that executes math operations. The math operation must be received as input parameter whose function type corresponds to (Int, Int) -> Int. Besides the math operation, the function must receive an integer corresponding to the number that will be passed in to the math operation:

When calculate function gets invoked, it prints out the phrase "Starting calculation..." and then it executes the math operation by calling operation parameter whose result ends up being returned.

We can invoke calculate function like this:

The first lambda — Lines 1 to 4 — corresponds to the multiplication. The second lambda — Lines 5 to 8 — corresponds to the addition. In this way, when the function calculate is invoked — Lines 10 and 11 — we can pass in any of these lambdas since both function types match the operation parameter.

The previous example outputs the following:

In Kotlin, a high-order function can be declared with more than one function types, even in combination with other data types, however, as long as the last input parameter corresponds to a function type, this can be placed out of the parentheses during its invocation. This notation is known as trailing lambda.

This way, you can skip the variable declaration and assignment. Although you can do the same thing without taking the lambda out of the parentheses.

If you have worked with collections, then you probably have seen operators like map , filter , foreach , etc. All these operators are high-order functions that receive function types as the last input parameter. That’s why we can call them with a trailing lambda notation:

🔗 If you want to learn more about this topic, take a look at the section High-order functions and lambdas of Kotlin’s official documentation at https://kotlinlang.org/docs/lambdas.html

Extension functions

Through extension functions, Kotlin allows us to add functionality to any class, even when that class can’t be modified. An extension function is a regular function whose name is preceded by the class name that it is supposed to extend.

Let’s illustrate extension functions with a practical example:

Imagine that you are developing a game which displays phrases as Strings whose Chars are randomly shuffled. The objective of the game is to reorder all the Chars until all the phrases are correctly formed. With a String we can do multiple things, like splitting it with the function split, finding a Char with the function find, extracting a substring with the function substring, etc., but there is not a function to shuffle its Chars.

In the absence of a function to shuffle its Chars, we can take advantage of the extension functions by implementing that functionality ourselves. In fact, if you take a look at all the String functions that I just mentioned, you will find out that these functions are extension functions.

Continuing with the practical example, we can create a String extension function that randomly shuffles its Chars as follows:

Notice that the name of the shuffled function is preceded by the String prefix, connecting both by a dot String.shuffled. When a function is defined in the form of an extension function, the result is the same as if that function was defined within the class. We have added more functionality to the String class without modifying it, and the best part is that this function can be applied on any String in any part of the project. Now we can shuffle our Strings with a single line of code that looks clear.

With extension functions we can add functionality to any class that we can’t modify. Also we can add functionality to interfaces and although this might seem unnecessary, we can even add functionality to any class that can be modified.

Now let’s take a closer look at the shuffled function example. On line 5 you can see the following: sb.append(this[it]). The important thing to notice here is the implicit receiver this. The String can be referenced by the this keyword just like if the functions were defined within the String class.

At this point I suppose you’re wondering what would happen if an extension function was named the same way as a function already defined within the class and with the exact same signature. If you do that, the compiler won’t throw an error and a RuntimeException won’t be thrown either, but the function that gets invoked is the one defined within the class.

Extension functions, like regular functions, can receive input parameters (or not) and return any value (or not).

Now that you know this powerful tool, use it to create more intuitive and less verbose code. You will no longer need all those classes with helper functions that we used to name “Utils” and that we were never sure about where to place them.

🔗 If you want to learn more about this topic, take a look at the section Extensions of Kotlin’s official documentation at https://kotlinlang.org/docs/extensions.html#extension-functions

Lambdas with receiver

Now that you know how to define a lambda and an extension function, you can combine both to make what is known as lambda with a receiver or extension function type.

Let’s take the same practical example of the previous section. This time we will define a lambda instead of an extension function. This lambda receives a String as an input parameter and returns another String with its Chars shuffled. Its function type is the following: (String) -> String.

Since we omitted the arrow notation, its input parameter is named it by default. If we wanted to create a lambda with a receiver and make that receiver be the String involved in the operation, then we have to move the input parameter out of the parentheses like this: String.() -> String. Now we can access the String through the implicit receiver this.

In this case, the lambda can be invoked like this: val shuffledText = "This is my text!".shuffled().

A lambda with a receiver can have only one receiver. Any other data must be passed in as input parameters enclosed in parentheses.

🔗 If you want to learn more about this topic, take a look at the section Function literals with receiver of Kotlin’s official documentation at https://kotlinlang.org/docs/lambdas.html#function-literals-with-receiver

Scope functions

If you take a closer look at extension functions or at lambdas with receiver, you will notice that their code blocks are in the scope of the object that invokes them. Their implicit receiver this is used to invoke and access all its methods and properties.

In Kotlin, there are 5 scope functions that we can use to work inside the scope of an object. These functions are meant to help us write less verbose and more idiomatic code. These 5 functions are let, run, with, apply, and also.

To access all methods and properties of the object, with, run and apply functions use the implicit receiver this, while let and also use its reference it. Their return values depend on each function. let, run and with functions return their last instruction’s value, just like any lambda, while apply and also functions return their receiver.

Let’s illustrate all this with one example for each scope function:

  • apply:

A StringBuilder object invokes the apply function. Inside apply’s trailing lambda, 3 Strings are appended — Lines 2, 3 and 4 — . The apply function is perfect for setting all the properties of an object recently created. Since all its properties can be accessed through the implicit receiver this, it can be omitted during invocations — Lines 3 and 4 — . This pattern is very common and very important when building a DSL.

  • run:

In this case we take the same previous example, but this time with the run function. The only difference between apply and run is that the latter returns its last instruction’s value — Line 5 — instead of its receiver.

  • let:

let function behaves just exactly like run function. Their only difference between them is that let accesses its receiver’s scope through its it reference.

  • also:

also function works like apply function when it comes to their return value, however, to access its scope it behaves like let function, that is, it has to do it through its it reference.

  • with:

with function is different from the other 4 scope functions in the way its receiver is passed in. It has to be passed in as an input parameter enclosed in parentheses. Inside its trailing lambda, its scope is accessed through its implicit receiver this and it returns its last instruction’s value.

🔗 If you want to learn more about this topic, take a look at the section Scope functions of Kotlin’s official documentation at https://kotlinlang.org/docs/scope-functions.html

Builder design pattern

Generally, when we have a POJO or a data class with several immutable properties, its creation can become prone to errors especially when some of its properties are the same type. In cases like this, the possibility of swapping properties by mistake gets increased. To avoid this, the builder design pattern is applied acting as an intermediary that allows us to create an object setting property by property.

Let’s illustrate this pattern with an example. We’re going to simulate a meal preparation, specifically the breakfast, skipping the rest of the meals for simplicity:

In this case I decided to define a sealed class, however, you could define an interface or an abstract class.

To create an instance of Breakfast class, it is necessary to pass in all its parameters to its constructor.

An instance of Breakfast class requires 4 input parameters to be created, although it is very common to find very complex objects that create other objects. In this case, the first two input parameters are Boolean and there is a possibility of swapping them by mistake. As its input parameters get increased, making a mistake becomes more likely.

Two very important aspects that you should pay attention to are the following:

  • A builder declares the exact same properties with default values.
  • All setters return their receiver. This allows them to be chained.

An instance creation through this intermediary class can be done like this:

Now a Breakfast instance creation is more intuitive, however, Kotlin lets us define this pattern with even less code:

We make all its properties public, so we don’t need to specify their setters.

Now with the scope function apply we can create an instance of Breakfast as follows:

It’s not a coincidence that the final code looks very similar to the code that we write when we’re creating objects in a DSL, since this design pattern is intensively applied during its implementation.

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