Kotlin — Understanding the magic of the mysterious Lambda with receiver

Paul Newport
5 min readFeb 1, 2024

--

A lambda with a receiver is a core Kotlin concept. Understanding how they work is crucial in taking your Kotlin skills to the next level. In this article, I’ll describe what a lambda receiver is, and how it allows you to write simple, easy to understand code.

Let’s start off by looking at a simple function in Kotlin, one that multiplies two numbers together:

fun multiply(x:Int,y:Int) : Int {
return x * y
}

Looking at this function we can see it has a name “multiply”, two parameters of type Int, and returns a type of Int. We can define the signature of the function like so:

(Int,Int)->Int

Let’s look at an even simpler function:

fun helloWorld() {
println()
}

We can see that this function takes no parameters, and doesn’t return anything either. In Java, a method that doesn’t return anything is defined as being void. In Kotlin, there is a special type called Unit, which is used to indicate the function doesn’t return anything. So you could write helloWorld like this

fun helloWorld() : Unit {
println()
}

Kotlin allows you to leave out Unit if you want.

So what’s the signature of the helloWorld function? It takes no parameters, and returns Unit, so it’s this:

()-> Unit

When coding lambdas with receivers, it’s crucial to understand function signatures, so it’s worth letting the above sink in, especially the last example which is used a lot when you come to writing DSLs.

Imagine you’re writing the software for a pocket calculator. When you press the x key, you want to multiply something. Let’s write a calculator function that takes in two numbers as parameters, and a function that does a mathematical operation on those two numbers, and returns a result. An example of such a function could be the multiply function shown earlier. Here’s the function:

fun calculate(x: Int,y: Int,operation: (Int,Int)-> Int): Int {
return operation(x,y)
}

Calculate is a function that takes 3 parameters. The first two are the Ints,x and y. The third parameter is a function, that takes two Ints and returns an Int. The function body runs the operation and returns the result of it.

As our multiply function has a signature that matches that of operation, we can pass that in. Kotlin allows you to conveniently refer to the function via a reference, as below:

 val six = calculate(3,2,::multiply)

What happens if, instead of passing in an existing function, you want to pass in a function on the fly, in other words a lambda ? This is easy, just pass in the lambda, signified by two braces {}, names for each parameter, x and y, and the body of the function, after the ->

val five = calculate(3,2,{x,y->x+y})

Remember in Kotlin that if the last parameter of a function is a function, you can pass the function after the closing brace, so it looks a bit nicer:

val five = calculate(3, 2) { x, y -> x + y }

Armed with this knowledge, we are almost ready to look at lambdas with receivers. But but quite yet. Let’s just remind ourselves a bit about extension functions.

An extension function is a handy way of adding a new function to an existing class. Suppose we want to modify the String class to have a new function, reverseAndUpper. We can’t modify the source code of String as it’s part of the language, but we can write an extension function instead, which will achieve the same thing:

fun String.reverseAndUpper(): String {
return this.reversed().uppercase()
}

Look at the syntax of this. We have a class “String”, a full-stop aka period, then the name of the extension function, then the parameters (none in this case) and finally the return type. Pay attention to this bit String.reverseAndUpper(). It means “this is a function that works on objects of type String”.

We are almost there ! Lets write some builder code, firstly without using a lambda and receiver.

data class Person(val name: String, val address: Address)
data class Address(val street: String, val city: String, val country: String)

@DslMarker
annotation class PersonDsl

@PersonDsl
class PersonBuilder {
lateinit var name: String
private lateinit var address: Address

operator fun invoke(block: PersonBuilder.() -> Unit): Person {
val personBuilder = PersonBuilder()
personBuilder.block()
return personBuilder.build()
}

fun address(block: (AddressBuilder) -> Unit) {
val builder = AddressBuilder()
block(builder)
address = builder.build()
}

private fun build(): Person {
return Person(name, address)
}
}
@PersonDsl
class AddressBuilder {
lateinit var street: String
lateinit var city: String
lateinit var country: String

fun build(): Address {
return Address(street, city, country)
}
}

fun main() {
val personBuilder = PersonBuilder()

val jim = personBuilder {
name = "Jim"
address {
it.street = "Street"
it.city = "City"
it.country = "Country"
}
}
println(jim)
}

There are a couple of data classes, and builders associated with each data class. In order to build the Address object on person, there’s a function called address, that takes as a parameter a function that takes a parameter of an address builder:

fun address(block: (AddressBuilder) -> Unit)

When the address builder is called, a block of code (the lambda) can be passed to set the values on the address builder:

address {addressBuilder->
addressBuilder.street = "Street"
addressBuilder.city = "City"
addressBuilder.country = "Country"
}

// or

address {
it.street = "Street"
it.city = "City"
it.country = "Country"
}

As we are calling a function with a function , we have to reference that function within the code, either explicitly as addressBuilder, or implicitly using it. Either way it doesn’t look nice, wouldn’t it be better if the builder variable could somehow be left out? Like this:

val jim = personBuilder {
name = "Jim"
address {
street = "Street"
city = "City"
country = "Country"
}
}

Enter the lambda with receiver — we finally got there

The address function can be re-written like this:

fun address(block: AddressBuilder.() -> Unit) {
val builder = AddressBuilder()
builder.block() // block is an extension function on AddressBuilder
address = builder.build()
}

Look at the block parameter AddressBuilder.()-> Unit — let’s split it into two sections:

AddressBuilder.
() -> Unit

Working backwards, () -> Unit means this is a function that takes no parameters and doesn’t return anything, just like the helloWorld function at the top of the article. The AddressBuilder. section means: the lambda we are passing in is in effect, an extension function on AddressBuilder, and therefore has access to all the properties of AddressBuilder. AddressBuilder can therefore be referred to as “this”. With this in mind, we can call the function like so.

address { // this is addressBuilder, and you can leave out this.
street = "Street"
city = "City"
country = "Country"
}

We now have the basis of a type safe DSL, with IDE help, that is easy to read, and easy to use.

Lambdas with receivers are used all over the place in Kotlin, especially in DSLs. Have a look in your project’s build.gradle.kts and see how many you can spot. Scope functions (the subject of my next article) such as apply and run also use them.

Conceptually they are a bit tricky to understand,I’ve found the best way to understand them is to write a simple DSL, like the one above.

Good luck, and as ever, happy coding in Kotlin !

--

--

Paul Newport

Software developer since the stone age. Kotlin and Spring fan.