Lambdas at Work: Transforming Collections
In part one we learned that a lambda is a value: code you can store in a variable and call. But a lambda you only ever call yourself isn’t worth much. The real power shows up when you hand a lambda to another function and let it do the calling. Kotlin’s collection library is built almost entirely around this idea, and it’s where you’ll use lambdas every single day.
This is part two of five. By the end you’ll be writing the fluent list.filter { }.map { } style that Kotlin is known for — and you’ll understand exactly what it means.
Functions that take functions
A function that accepts a lambda as a parameter is called a higher-order function. The simplest one on a list is forEach: you give it a lambda, and it runs that lambda once for each element.
val names = listOf("Ada", "Linus", "Grace")
names.forEach({ name -> println(name) })
Look closely: forEach is being called with one argument, and that argument is a lambda — exactly the kind of value from part one. forEach takes care of the looping; your lambda just says what to do with each element.
The trailing-lambda convention
That call works, but Kotlin developers almost never write it that way. There’s a convention: when a lambda is the last argument to a function, you can move it outside the parentheses. And if it’s the only argument, the parentheses disappear entirely:
names.forEach { name -> println(name) }
Add the it shortcut from part one, and it gets shorter still:
names.forEach { println(it) }
This single convention is why idiomatic Kotlin looks the way it does — those trailing { } blocks you see everywhere are just lambdas passed as the last argument. Once you recognize the pattern, a lot of “magic-looking” Kotlin becomes ordinary function calls.
map: transform every element
forEach does something with each element but gives nothing back. map is the workhorse that transforms: it runs your lambda on each element and collects the results into a new list.
val numbers = listOf(1, 2, 3, 4)
val squares = numbers.map { it * it } // [1, 4, 9, 16]
Your lambda ({ it * it }) describes how to turn one element into a new value; map applies it to all of them. The original list is untouched — map returns a new one.
filter: keep only some
filter takes a lambda that returns true or false, and keeps only the elements for which it’s true:
val evens = numbers.filter { it % 2 == 0 } // [2, 4]
Chaining them together
Because map and filter each return a new list, you can chain them — and the code reads top-to-bottom in the order things happen:
val result = numbers
.filter { it % 2 == 0 } // keep evens: [2, 4]
.map { it * it } // square them: [4, 16]
This is the heart of everyday Kotlin: describe what you want as a pipeline of small transformations, instead of writing a loop with a mutable list and an index.
The rest of the toolkit
map and filter are two of many, and they all follow the same shape — a method that takes a lambda. A few you’ll reach for often:
numbers.any { it > 3 } // true — is any element > 3?
numbers.all { it > 0 } // true — are all of them > 0?
numbers.count { it % 2 == 0 } // 2 — how many are even?
numbers.find { it > 2 } // 3 — the first match, or null
numbers.sumOf { it * 2 } // 20 — add up a value per element
You don’t need to memorize the list. The pattern is what matters: a collection method that takes a lambda describing the per-element rule. When you need one, you’ll guess its name half the time and find it in autocomplete the other half.
These functions aren’t magic — you can write them
It’s worth seeing that higher-order functions are nothing special; you can write your own. Here’s one that runs an action a given number of times:
fun repeatTimes(n: Int, action: (Int) -> Unit) {
for (i in 0 until n) action(i)
}
repeatTimes(3) { println("tick $it") }
The parameter action: (Int) -> Unit is just a lambda type from part one — this function accepts a lambda and calls it. The trailing-lambda convention lets the caller pass it in those clean braces. There’s no compiler trick here; the collection functions work the exact same way.
A lambda also remembers
There’s one more property worth flagging before we go further: a lambda can use variables from the scope where it was defined — it “closes over” them. A lambda passed to filter, for instance, can reference a threshold from the surrounding function. That ability, called a closure, behaves differently in Kotlin than in Java and is subtle enough to cause real bugs, so it gets its own stop next.
Final thoughts
This is the part of lambdas you’ll use most: hand a small lambda to a collection function and let it handle the looping, the new list, the bookkeeping. map transforms, filter selects, and a dozen siblings cover the rest — all the same shape, all just higher-order functions you could have written yourself. And because lambdas close over their surroundings, they can pull in exactly the context they need.
Next: closures — what it means for a lambda to “remember” the variables around it, why Kotlin lets you change them when Java won’t, and the surprises that follow.
Practice: reinforce this with the companion workbook — short, click-to-reveal problems.
Comments