Kotlin Lambdas Get Clever: References, Receivers, and inline
Part one built the lambda up from a value you can call to the higher-order functions you use daily. This part covers the features that make Kotlin lambdas feel effortless: skipping them when a function already exists, the surprising rules for return, lambdas that read like native syntax, and the reason none of it costs much.
Function references: when the lambda is just a call
If a lambda’s only job is to call an existing function, you can hand over that function directly with :::
fun isEven(n: Int) = n % 2 == 0
nums.filter { isEven(it) } // a lambda that calls isEven
nums.filter(::isEven) // a reference to isEven — same result
References work for members and constructors too:
listOf("a", "bb").map(String::length) // [1, 2]
listOf("Ada", "Lin").map(::User) // calls User("Ada"), User("Lin")
String::length references the property’s getter; ::User references the constructor. When the function you want already exists, a reference is shorter and says exactly what it means.
return inside a lambda
This one surprises people. A bare return in a lambda does not return from the lambda — it returns from the enclosing function:
fun firstEven(nums: List<Int>): Int? {
nums.forEach {
if (it % 2 == 0) return it // returns from firstEven, not the lambda
}
return null
}
That’s a non-local return, and it’s usually exactly what you want. When you only mean to skip the current element, use a label:
nums.forEach {
if (it % 2 != 0) return@forEach // like 'continue' — on to the next element
println(it)
}
return@forEach returns from the lambda itself, leaving the outer function running.
Anonymous functions: when you want the normal rules
A lambda can’t declare its own return type, and its return is non-local. When you need either, reach for an anonymous function instead:
val parse = fun(s: String): Int? {
if (s.isBlank()) return null // a local return, and an explicit type
return s.trim().toIntOrNull()
}
It’s still a function value, just written with the fun keyword — which means it follows the ordinary rules for return.
inline: why lambdas are basically free
Passing a lambda sounds like it should allocate a function object and add a call. For library functions marked inline, it doesn’t:
inline fun <T> measured(block: () -> T): T {
val start = System.nanoTime()
val result = block()
println("took ${System.nanoTime() - start} ns")
return result
}
At each call site, the compiler copies the function’s body — and the lambda’s body — directly into place. No function object, no extra call. Most of the collection operators (map, filter, forEach) are inline, which is why chaining them stays cheap. It’s also what makes those non-local returns possible: the lambda really is compiled into the caller.
Lambdas with a receiver
This is the feature behind Kotlin’s nicest APIs. An ordinary function type is (T) -> R. A receiver type is T.() -> R — inside the lambda, T becomes this, so you call its members with no qualifier:
fun buildName(block: StringBuilder.() -> Unit): String {
val sb = StringBuilder()
sb.block() // run the lambda with sb as the receiver
return sb.toString()
}
val name = buildName {
append("Ada") // 'this' is the StringBuilder
append(" Lovelace")
}
No it, no prefix — the body reads as if you were writing inside StringBuilder. This is the mechanism behind apply, buildString, and every Kotlin DSL you’ve seen, from HTML builders to Gradle Kotlin scripts.
The scope functions, briefly
The standard let, run, with, apply, and also are nothing exotic — they’re small higher-order functions built on everything above, most of them lambdas with a receiver. A quick map:
apply { }— receiver isthis, returns the object. Configure-then-return.also { }— argument isit, returns the object. Side effects in a chain.let { }— argument isit, returns the lambda’s result. Transform, or null-safe chains.run { }/with(x) { }— receiver isthis, returns the lambda’s result.
Seen as ordinary functions taking a (sometimes receiver) lambda, the “which one do I use” anxiety mostly dissolves: choose by whether you want this or it, and whether you want the object or the result back.
Final thoughts
“A lambda is a value you can call” carries much further than it first appears. References let you drop the lambda when a function already exists. Receivers turn a lambda into something that reads like native syntax. inline makes the whole arrangement essentially free. None of these are separate features bolted on — they’re what falls out of taking functions are values seriously.
So the next time a Kotlin API feels like magic — a builder, a DSL, one of the scope functions — look closely. It’s almost always one of these lambdas underneath.