Kotlin DSL - let's express code in "mini-language" - Part 2 of 5

Kotlin DSL - let's express code in "mini-language" - Part 2 of 5

In this second post of this series , we take a look at the building blocks for our DSLs

The fundamental building blocks we have in Kotlin for building DSLs are Function Literals with Receiver,  the invoke convention , infix notation and extension functions.

Extensions functions

A handy way of extending existing classes with a new functionality without using inheritance or any forms of the Decorator pattern – after defining an extension. we can essentially use it – as it was the part of the original API.

fun Int.double() {
  return this * 2
}
2.double() == 4

This basically means: add a method to the type Int with name double, which you can then invoke on any Int. The keyword this is used to get the instance on which you’re invoking the method (the receiver object). How this works in more detail is compiling to a static method of which this is the first parameter.

Function literals with Receiver

In Kotlin, functions are first-class citizen. It means that functions can be assigned to the variables, passed as an argument or returned from another function. While Kotlin is statically typed, to make it possible, functions need to have a type. It exists and it is called function type. Here are a few examples:

  • ()->Unit —the function type that returns nothing useful (Unit) and takes no arguments.
  • (Int)->Int— the function type that returns Int and takes single argument of type Int.
  • ()->()->Unit— the function type that returns another function that returns nothing useful (Unit). Both functions take no arguments.

These function types can be used as parameters to other functions, which are in turn called “higher order functions”:

fun myHigherOrderFun(functionArg: (Int)->String) = functionArg(5)

As you can see myHigherOrderFun has a parameter of a function type, which it can call in its method body. If we now want to use this higher order function, we can make use of lambdas, also referred to as “function literal”:

println ( myHigherOrderFun { "The Number is $it" })
>> prints "The Number is 5"

Another way to provide a function is to use function literal. Generally, literal in programming is a syntactic sugar for representing values of some types the language considers particularly important. Therefore function literal is a special notation used to simplify how a function is defined. There are two types of function literals in Kotlin:

Lambda expression is a short way to define a function.

Anonymous function is an alternative way to define a function.

As we can see, lambda expression and anonymous functions are pretty similar. Why are they distinguished? Generally the big difference is that anonymous functions are more explicit. It is more clear when we are using them, and return value needs to be specified explicitly. Lambda expression returns value of the last statement in its body or Unit.

Kotlin goes even further and supports the concept of function literals with receivers. This enables us to call methods on the receiver of the function literal in its body without any specific qualifiers. This is very similar to extension functions, where it’s also possible to access members of the receiver object inside the extension.

Let’s see, what function literals look like:

var greet: String.() -> Unit = { println("Hello $this") }

This one defines a variable of type String.() -> Unit, which is basically a function type () -> Unit with String as the receiver. All methods of this receiver can, therefore, be called in the method body. In this example, this is simply used to print the String. The function can be called like so:

greet("Adit") 
>> prints "Hello Adit"

This is essentially all you need to know, let’s see a use case in action.

Let's take apply as an example

Kotlin’s standard library contains methods using this concept, one of which is apply.

public inline fun <T> T.apply(block: T.() -> Unit): T { 
    block(); 
    return this 
}

apply can be used as follows

uiLabel.apply {
    textColor = Color.BLACK
    backgroundColor = Color.GREEN
    text = ""
}

As we can easily see, it’s an extension function to everything literally, and it expects a function literal with a generic receiver of type T, which is run before the receiver is returned to the caller. This little function is actually fantastic as it provides a way to build certain objects very concisely and easily.

println(StringBuilder("Hello ")
        .apply {
            append("Kotliner")
            append("! ")
            append("How are you doing?")
        }.toString())
>> prints "Hello Kotliner! How are you doing?"

In this example, a StringBuilder is created and then the apply method is called on it. As we’ve seen before, it’s possible to call any method of our receiver StringBuilder like append in the above code. Since apply returns the receiver after it completes, a call of toString on the StringBuilder can immediately be performed and the text is printed to the console.

Invoke

An interesting feature of the Kotlin language is the ability to define an invoke operator , which allows us to call an object or an expression as a method. When you specify an invoke operator on a class, it can be called on any instances of the class without a method name!
This trick seems especially useful for classes that really only have one method to be used.

For example, consider the following example:

data class Fraction(val numerator: Int, val denominator: Int) :  Comparable<Fraction> {
    //...
    operator fun invoke(prefix: String = "") = println(prefix + toString())

}

In this invoke operator function a single parameter of type String is defined. The parameter is used to prefix the toString() representation of a Fraction instance and print it to the console. So, how do we use this functionality?

var fraction = Fraction(2, 3)
fraction("Fraction is: ")
>> prints "Fraction is: 2/3"

The compiler translates this call to the more comprehensive expression

fraction.invoke("Fraction is : ")

Another simple example :

val sum = { x: Int, y: Int -> x + y }
sum.invoke(3, 10)
sum(3, 10)

Infix functions

Functions marked with the infix keyword can also be called using the infix notation (omitting the dot and the parentheses for the call). Infix functions must satisfy the following requirements:

Example of infix notation in Kotlin

infix fun String.shouldBeSame(other: String) = this == other

// calling the function using the infix notation
"bello" shouldBeSame "bello" //returns true

// is the same as
"hello".shouldBeSame("hello") //returns true

On the left side, is the receiver instance on which you’ll define infix function. On the right side, is a parameter that will be passed to the infix function.

You can think the best use case for infix notation is to make argument matchers in testing frameworks more readable and create nice looking APIs.

In Part 3 of this series , we will take a look at some amazing use case for DSLs in Android.