1. Overview
In this quick tutorial, we’re going to see what lambdas with receivers are and how they result in more simplicity and readability.
2. Lambda with Receivers
Let’s start with a vanilla extension function in Kotlin that accepts a lambda as the input:
fun <T> T.applyThenReturn(f: (T) -> Unit): T {
f(this)
return this
}
With this extension function, we can apply a function to a value and return the same value:
val name = "Baeldung".applyThenReturn { n -> println(n.toUpperCase()) }
As shown above, to operate on the value, we have to use the lambda argument. *Normal Lambda functions in Kotlin are just like that: a set of explicit arguments and the body of the lambda separated by a ->.*
This is nice, but what if we could do something like:
val name = "Baeldung".applyThenReturn { println(toUpperCase()) }
Instead of operating on a lambda argument (n in the above example), here we’re calling the toUpperCase() method on the this reference. Basically, we’re pretending that each unqualified function call is using the “Baeldung” string as the receiver of the call. This makes the lambda body more concise.
As it turns out, this form of the lambda expression is actually possible in Kotlin by lambda with receivers. In order to use this feature, we should define the lambda expression a bit differently:
fun <T> T.apply(f: T.() -> Unit): T {
f() // or this.f()
return this
}
As shown above, we moved the type parameter outside of the parentheses. Also, instead of f(this), we just call the f() function which is equal to this.f(). Again, each unqualified function call uses an instance of T as the call receiver.
Both the standard library and third-party libraries have extensively used lambda with receivers to make a better developer experience. A couple of built-in scope functions in Kotlin are using this feature, including the apply() scope function:
inline fun <T> T.apply(block: T.() -> Unit): T {
// omitted
block()
return this
}
3. Bytecode Representation
To see how these two functions are different at the bytecode level, let’s compile each of them. To do that, we can use the kotlinc utility. After compilation, we can use the javap tool to take a peek at the generated bytecode:
>> kotlinc Receiver.kt
>> javap -c -p com.baeldung.receiver.ReceiverKt
public static final <T> T applyThenReturn(T, kotlin.jvm.functions.Function1<? super T, kotlin.Unit>);
Code:
// omitted
6: aload_1
7: aload_0
8: invokeinterface #49, 2 // InterfaceMethod Function1.invoke:(Ljava/lang/Object;)Ljava/lang/Object;
So, as we can see, the Kotlin compiler translates the applyThenReturn function to a static method accepting two parameters: one to pass the extension function receiver and the other encapsulating the lambda body. Here, the Function1<? super T, kotlin.Unit> is a function with one input that returns nothing or Unit.
Moreover, to call the lambda, it simply passes the first parameter (aload_0) to the invoke method. Surprisingly, the bytecode for the lambda with the receiver is exactly the same as above.
Similarly, one the call site, Kotlin translates both function calls to simple static method invocations.
The bottom line is, even though normal lambdas and lambda with receivers are different at compile-time, they’re exactly the same at the bytecode level.
4. Conclusion
In this tutorial, first, we saw how we can take advantage of the lambda with receivers to make better and more readable program constructs. In addition to getting familiar with the API, we also learned how this type of lambda is represented at the bytecode level.
As usual, all the examples are available over on GitHub.