Lambdas, Sharpened: References, Returns, and Anonymous Functions
By now you can write a lambda, hand it to a function, and reason about what it captures. This part covers three things that make lambdas nicer to write, and one that genuinely surprises people coming from Java. None of it is hard once you’ve seen it; the goal is that nothing trips you up later.
This is part four of five.
When a function already exists: references
Sometimes a lambda does nothing but call a function that already exists:
val words = listOf("ada", "linus")
words.map { it.uppercase() }
That lambda — { it.uppercase() } — is pure overhead; all it does is forward to uppercase. Kotlin lets you point at the function directly with ::, the function reference operator:
words.map(String::uppercase)
String::uppercase is a value of the right type — a function that takes a String and returns one — so you can pass it wherever a lambda fits. There are four flavors, and they’re all the same idea, “refer to a function instead of wrapping it”:
// 1. A top-level function — :: in front of its name
fun isEven(n: Int) = n % 2 == 0
numbers.filter(::isEven)
// 2. A member, by its type — Type::member
words.map(String::uppercase)
// 3. A member, bound to one object — instance::member
val logger = Logger()
messages.forEach(logger::log)
// 4. A constructor — :: in front of the type name
val ids = listOf(1, 2, 3)
val users = ids.map(::User) // calls User(id) for each
The rule of thumb: if your lambda is just { someFunction(it) }, you can usually replace it with ::someFunction. If it does anything more, keep the lambda.
The surprise: return inside a lambda
Here’s the one that catches Java developers. You might expect return inside a lambda to exit just the lambda, the way returning from an anonymous class’s method would. It doesn’t. A plain return inside a lambda returns from the enclosing function:
fun findFirstEven(numbers: List<Int>): Int? {
numbers.forEach {
if (it % 2 == 0) return it // returns from findFirstEven, not the lambda
}
return null
}
That’s actually useful here: return it jumps straight out of findFirstEven with the answer. This behavior is called a non-local return, and it works because functions like forEach are inlined (the subject of part five). For now, just know the rule: plain return in a lambda exits the function around it.
Labels: returning from just the lambda
But what if you only want to skip the current element — the equivalent of continue in a loop? For that you return from the lambda itself, using a label. Every lambda passed to a named function gets an automatic label matching that name:
numbers.forEach {
if (it < 0) return@forEach // skip this element, continue the loop
println(it)
}
return@forEach means “return from the forEach lambda,” not from the enclosing function. The @forEach part is the label. Read it as “return, but only out of this block.”
So the two behaviors sit side by side:
return— leaves the whole enclosing function (non-local).return@forEach— leaves only the lambda, moving on to the next element.
This is the single most confusing thing about Kotlin lambdas, so it’s worth pausing on until it clicks: the @label is how you choose which thing you’re returning from.
Anonymous functions: the normal rules, on demand
If the non-local return bothers you, there’s an alternative that behaves the way Java taught you. An anonymous function is a function with no name, written with the fun keyword:
val isEven = fun(n: Int): Boolean {
return n % 2 == 0 // returns from the anonymous function — normal rules
}
It’s interchangeable with a lambda as a value, but inside it, a plain return returns from itself, exactly as a regular function would. The trade-off: it’s more verbose, and it can’t be used for non-local returns. In practice lambdas are the default and anonymous functions are the occasional tool; reach for one when you specifically want the ordinary return behavior, or when you need to write the return type explicitly.
Final thoughts
Three tools, one of them a genuine gotcha. References (::) let you drop the lambda when a function already exists. Labels (return@forEach) let you choose whether return exits the lambda or the whole function — the rule being that a bare return leaves the enclosing function. And anonymous functions give you back Java’s familiar return semantics when you want them.
One question remains: we keep passing all these lambdas around — doesn’t creating an object for each one cost something? The answer is the key to the whole design. Next: why lambdas are free, and lambdas that read like syntax.
Practice: reinforce this with the companion workbook — short, click-to-reveal problems.
Comments