map, filter, reduce and friends
This is the lesson that changes how you write Kotlin. The functional collection operations — map,
filter, reduce, and their many friends — let you say what you want done to a collection rather
than how to loop over it. Once these are second nature, you will write a fraction of the loops you
would in other languages, and your code will read like a description of the result.
Every one of these is a higher-order function taking a lambda, exactly as the functions module set up. They come in families by what they produce.
Transforming: map
map applies a lambda to every element and returns a new list of the results:
val prices = listOf(100, 250, 90, 500)
val withTax = prices.map { it + it / 10 } // [110, 275, 99, 550]
val labels = prices.map { "₹$it" } // [₹100, ₹250, ₹90, ₹500]
val lengths = listOf("Pune", "Mumbai").map { it.length } // [4, 6]
map is the workhorse: "give me each of these, transformed". The result is always the same size as
the input (one output per input), and always a new list — the original is untouched.
Its cousin mapNotNull transforms and drops nulls in one step — invaluable with the null-safety
module:
val inputs = listOf("42", "abc", "17", "x")
val numbers = inputs.mapNotNull { it.toIntOrNull() } // [42, 17] — non-numbers dropped
Selecting: filter
filter keeps the elements for which the lambda returns true, returning a new, possibly
smaller list:
val prices = listOf(100, 250, 90, 500, 175)
val expensive = prices.filter { it > 200 } // [250, 500]
val cheap = prices.filterNot { it > 200 } // [100, 90, 175] — the opposite
filter is "keep the ones matching this condition". Chain it with map and you have replaced most
loops you would ever write:
// "the names of adults, uppercased"
people.filter { it.age >= 18 }.map { it.name.uppercase() }
Read left to right: filter to adults, then map to uppercase names. This one line replaces a loop, a condition, a mutable list, and an accumulation — and it says what it does.
Aggregating to a single value: reduce and fold
These collapse a whole collection into one value.
val numbers = listOf(1, 2, 3, 4, 5)
val sum = numbers.reduce { acc, n -> acc + n } // 15
val product = numbers.reduce { acc, n -> acc * n } // 120
reduce takes a lambda (accumulator, element) and threads the accumulator through: start with the
first element, combine with the second, combine that with the third, and so on. acc is the running
result.
fold is reduce with an explicit starting value, which is safer and more flexible:
val sum = numbers.fold(0) { acc, n -> acc + n } // 15, starting from 0
val total = orders.fold(0) { acc, o -> acc + o.amount } // sum a property
val csv = listOf("a", "b", "c").fold("") { acc, s -> acc + s } // "abc"
The crucial difference: reduce throws on an empty list (there is no first element to start
from), while fold returns the starting value for an empty list. So fold is the safer default,
especially when the collection might be empty.
In practice, for the common aggregations you rarely write reduce or fold by hand — the standard
library has direct operations that are clearer:
println(numbers.sum()) // 15
println(numbers.average()) // 3.0
println(numbers.max()) // 5
println(numbers.min()) // 1
println(orders.sumOf { it.amount }) // sum of a property, directly
println(orders.maxByOrNull { it.amount }) // the order with the largest amount
Prefer sum, average, sumOf, maxByOrNull and friends over a hand-written fold when one
exists — they say the intent directly. Reach for fold when you are building up something custom
that no dedicated operation covers.
Grouping and the rest
A few more you will use constantly:
val words = listOf("Pune", "Mumbai", "Nagpur", "Panaji", "Nashik")
// group by first letter -> Map<Char, List<String>>
println(words.groupBy { it.first() })
// {P=[Pune, Panaji], M=[Mumbai], N=[Nagpur, Nashik]}
println(words.sortedBy { it.length }) // shortest first
println(words.sortedByDescending { it.length })
println(words.count { it.length > 4 }) // 4
println(words.any { it.startsWith("P") }) // true
println(words.all { it.length >= 4 }) // true
println(words.take(2)) // [Pune, Mumbai] — first 2
println(words.distinct()) // duplicates removed
println(words.associateWith { it.length }) // {Pune=4, Mumbai=6, ...} — build a map
groupBy is especially powerful — it turns a list into a Map from key to the list of items with
that key, which replaces a fiddly manual loop-and-accumulate. associateWith builds a map from
elements to computed values. These are the kind of operations that, once you know they exist, save
you writing (and debugging) a dozen lines each.
The mindset shift
The change these bring is not just shorter code — it is thinking in transformations. Instead of
"loop over the list, and for each one, if it matches, add its name to another list", you think "the
names of the ones that match": filter { }.map { it.name }. You describe the destination, not the
journey. This is declarative style, and it is clearer, has fewer places for bugs (no mutable
accumulator to mismanage, no off-by-one), and reads like the requirement it implements.
The habit to build: when you reach for a for loop, pause and ask which operation says it. Map
to transform, filter to select, sumOf/groupBy/count to aggregate. A loop still earns its place
for side effects and genuinely custom logic — but far less often than instinct suggests.
Check your work
What map does, and the shape of its result. Transforms every element; returns a new list of the
same size.
What mapNotNull adds. Transforms and drops nulls in one step.
What filter does. Keeps elements matching the condition; returns a new, possibly smaller list.
What reduce does, and its danger. Collapses a collection to one value threading an accumulator;
throws on an empty collection.
Why fold is safer. It takes a starting value, so an empty collection returns that value instead
of throwing.
When to prefer sum/sumOf/maxByOrNull over fold. Whenever a dedicated operation exists —
it states the intent directly.
What groupBy produces. A Map from a key to the list of elements with that key.
The mindset shift. Describe the destination (declarative) rather than the loop (imperative) — fewer bugs, clearer code.
The habit. When reaching for a for loop, ask which collection operation says it.
Practice
mapa list of prices to add 10% tax, then to"₹"labels. Confirm the result is the same size.- Use
mapNotNull { it.toIntOrNull() }on a mixed list of numbers and non-numbers. filterprices over 200, then chain.map { }to label them. Read the chain left to right.- Sum a list with
reduce, then withfold(0). Then callreduceon an empty list and read the exception. Confirmfoldon empty returns the start value. - Replace a hand-written
foldthat sums withsumOf. Decide which reads better. groupBya list of city names by their first letter and print the resulting map.- Use
sortedBy,count,any,all,take, anddistincton one list, predicting each result first. - Take a real
forloop you have written that builds a list and rewrite it entirely with collection operations.
Official documentation
- Kotlin — Collection transformations —
map,groupBy,associateand more. - Kotlin — Filtering collections —
filter,filterNot,filterNotNull. - Kotlin — Collection aggregate operations —
sum,fold,reduce,maxByOrNull. - Kotlin — Collection ordering —
sortedByand friends.
Next: sequences, and when these operations are doing more work than they need to.
Stuck on this lesson?
Being stuck is part of it — but being stuck alone for three days is not. Our internship programme pairs this curriculum with code review and one-to-one help from working developers, and it is free.
About the internship