Why Lambdas Are Free, and Lambdas That Read Like Syntax
You can now write lambdas, pass them to functions, reason about what they capture, and use references, returns, and anonymous functions. Two pieces finish the picture: why all this lambda-passing is essentially free at runtime, and the one variation — lambdas with a receiver — that lets a lambda read like built-in syntax. This is the last part.
The hidden cost — and Kotlin’s answer
A lambda is a value, and values are usually objects. So a fair worry is: every time you call list.map { it * it }, does Kotlin allocate a little object for that lambda, plus make an extra function call per element? If it did, the fluent style from part two would be quietly expensive in hot loops.
Kotlin’s answer is the inline keyword. When a higher-order function is marked inline, the compiler doesn’t pass your lambda as an object at all — it copies the lambda’s body directly into the call site, as if you’d written the loop by hand.
inline fun repeatTimes(n: Int, action: (Int) -> Unit) {
for (i in 0 until n) action(i)
}
repeatTimes(3) { println("tick $it") }
Because repeatTimes is inline, the compiled code is effectively just:
for (i in 0 until 3) println("tick $i")
No lambda object, no extra call — the abstraction vanishes at compile time. Kotlin’s standard library marks map, filter, forEach, and the rest inline, which is why the expressive collection pipelines cost the same as a hand-written loop. It’s also what makes the non-local return from part four possible: since the lambda’s body is copied into the function, a return inside it really can return from the enclosing function.
You’ll mostly use inline functions rather than write them, but when you do write a higher-order function that’s called in tight loops, inline is the tool. (Two finer controls exist for the rare cases: noinline opts a single lambda parameter out, and crossinline restricts non-local returns. And inline is also what unlocks reified type parameters — a thread we picked up in generics.)
Lambdas with a receiver
Here’s the last idea, and it’s the one that explains a lot of “how does that even work?” Kotlin code. Recall a normal lambda type: (String) -> Unit — it takes a String as a parameter. A lambda with a receiver moves that type to the front with a dot:
String.() -> Unit
Read it as: “a lambda that runs on a String.” Inside such a lambda, the String isn’t a parameter you name — it’s this, the receiver, exactly like being inside a member function. And just as inside a class you can call methods without writing this., inside a receiver lambda you can call the receiver’s methods bare:
val build: StringBuilder.() -> Unit = {
append("Hello, ") // really this.append(...), but this is implicit
append("world")
}
You’ve already used this without knowing it. apply and buildString take exactly this kind of lambda:
val text = buildString {
append("Hello, ") // the receiver is a StringBuilder
append("world")
} // "Hello, world"
val file = File("out.txt").apply {
createNewFile() // the receiver is the File
setReadable(true)
} // returns the configured File
Inside those braces, append and createNewFile need no prefix because the lambda has a receiver. This is also the entire basis of Kotlin DSLs — the HTML builders, the Gradle Kotlin scripts, the test frameworks that read like plain English. They’re all just functions taking a lambda with a receiver, so the block can call the receiver’s methods as if they were keywords.
Where this leads: the scope functions
Once receivers click, the five scope functions — let, run, with, apply, also — stop looking like magic. They’re nothing more than tiny library functions, each taking a lambda that’s either a receiver lambda (this) or an ordinary one (it), and either returning the object or the block’s result. That’s a topic with enough nuance to deserve its own treatment, which it gets in the scope functions lesson.
Final thoughts
That completes the picture. A lambda starts as a plain value (part one), becomes powerful when handed to functions (part two), remembers the variables around it (part three), gains shortcuts and a return rule worth knowing (part four), and finally reveals that it costs nothing thanks to inline, and can carry a this to read like native syntax. Those two ideas — inlining and receivers — are what make Kotlin’s most expressive features, from the collection API to full DSLs, both pleasant and free.
With functions-as-values behind you, the next building block is the thing that holds them together. Next: classes, and why a Kotlin class is mostly its header.
Practice: reinforce this with the companion workbook — short, click-to-reveal problems.
Comments