Kotlin has built-in language features for creating custom DSLs. Receiver functions and the Infix keyword are two of those features.
Although intended primarily for creating DSLs, receiver functions can be also useful in everyday programming and the Kotlin standard library uses them in several utility functions.
We could say that receiver functions share some similarities with extension functions. They have the same syntax for marking the receiver and, inside the function, they can access members of the receiver instance. Here's how we'd define a receiver function type:
val hello: String.() -> Unit = { print("Hello $this") }
It's the same as a normal function type definition, with the addition of the receiver type and a dot before the function signature.
To call this function, we have to provide an instance of the receiver type, a string in our case:
hello("Kotlin") // prints Hello Kotlin
You can see how, inside the receiver function body, we have access to the instance of the receiver type with this
keyword.
Now, let’s see an example that shows a more real-world usage scenario. Let's write a function for building a string. It accepts a receiver function with StringBuilder
as the receiver type:
fun buildString(init: StringBuilder.() -> Unit): String { val builder = StringBuilder() init(builder) return builder.toString() }
We create the StringBuilder
instance inside the function, apply the receiver function to it, and then build the resultant string. This enables us to create a string object like this:
val string = buildString { append("Hello Receiver Functions") appendln("We have access to StringBuilder object inside this lambda") }
Notice how, inside the lambda, we are accessing members of the StringBuilder
type.
There are some Standard functions which accept receiver functions. Let's have a look at them.
Apply. This function extends the generic T
parameter with no constraints, which means that any Type declared in Kotlin can use it. It accepts a receiver function, and the receiver is the instance on which the apply function is called and then returns the same instance. Since you can access the instance of the object inside the apply
function, you can use it for creating objects and initializing them in the same place:
var stringBuilder = StringBuilder().apply { appendln("I'm the first line of the String Builder") }
Here we created an instance of the StringBuilder
class and used the apply function to append the first line.
With. This function can be used to call multiple functions of an object without specifying a receiver. The function accepts an object and a receiver function of the same type as the object:
val stringBuilder = StringBuilder() with(stringBuilder) { appendln("First Line") appendln("Second Line") appendln("Third Line") }
Because the lambda is a receiver function, inside the lambda we have access to the object that was passed as an argument. In this case, we call appendln
and the object that receives this function call is the one we pass in as the first argument.
With this function we can also return a different value. In the previous example, we didn’t have a return value. Here’s a similar example, but here a string is used as the return type:
val str: String = with(StringBuilder()) { appendln("First Line") appendln("Second Line") appendln("Third Line") toString() }