Collection Operations (map, filter, fold)
Reviewed & published by Brayan K
Kotlin is a modern, concise language whose standard library gives collections a rich functional API — map, filter, fold, and more — so you can transform and aggregate data without writing loops.
Part of the free Kotlin course at LearnCodingFast — hands-on lessons with worked examples and the output they print, plus practice exercises and a quick quiz.
By the end of this lesson you'll transform, filter, and reduce collections with clean, chainable operations.
What You'll Learn in This Lesson
- Transforming elements with map
- Selecting elements with filter
- Chaining operations into pipelines
- Aggregating with fold and reduce
- Grouping with groupBy
- Helpers: any, count, first, sum
1️⃣ map, filter, and Chaining
map applies a lambda to every element and returns a new list of results. filter keeps only the elements for which a lambda returns true. Because each returns a new collection, you can chain them into a readable pipeline.
fun main() {
val nums = listOf(1, 2, 3, 4, 5, 6)
// map transforms EACH element, producing a new list.
val doubled = nums.map { it * 2 }
println(doubled) // -> [2, 4, 6, 8, 10, 12]
// filter KEEPS elements where the lambda returns true.
val evens = nums.filter { it % 2 == 0 }
println(evens) // -> [2, 4, 6]
// Chain operations — each step returns a new list.
val result = nums
.filter { it > 2 } // [3, 4, 5, 6]
.map { it * it } // [9, 16, 25, 36]
println(result) // -> [9, 16, 25, 36]
// Other handy ops:
println(nums.first { it > 3 }) // -> 4
println(nums.any { it > 5 }) // -> true
println(nums.count { it % 2 == 0 }) // -> 3
}Helpers like first { }, any { }, and count { } answer common questions in a single, declarative line.
2️⃣ fold, reduce, and groupBy
To collapse a collection into a single value, use fold (with an explicit start) or reduce (which starts from the first element). groupBy partitions elements into a Map keyed by whatever the lambda returns.
fun main() {
val nums = listOf(1, 2, 3, 4)
// sum is built in:
println(nums.sum()) // -> 10
// fold builds a single result from an initial value + a combining lambda.
// (acc starts at 0; each step adds the next element)
val total = nums.fold(0) { acc, n -> acc + n }
println(total) // -> 10
// fold can build any type — here, a String:
val joined = nums.fold("") { acc, n -> acc + n }
println(joined) // -> 1234
// reduce is like fold but uses the first element as the start:
val product = nums.reduce { acc, n -> acc * n }
println(product) // -> 24
// groupBy partitions into a Map by a key the lambda computes:
val byParity = nums.groupBy { if (it % 2 == 0) "even" else "odd" }
println(byParity) // -> {odd=[1, 3], even=[2, 4]}
}The accumulator in fold can be any type — note how the same call built first a number, then a string. That flexibility makes fold the most general aggregation tool.
Your turn. Replace the TODO, then run and compare.
// 🎯 YOUR TURN — replace each TODO then run main()
fun main() {
val prices = listOf(10, 25, 5, 40, 15)
// 1) Keep only prices above 10
val expensive = prices.TODO { it > 10 } // 👉 replace TODO with 'filter'
println(expensive) // expect [25, 40, 15]
// 2) Apply a 10% discount to each (multiply by 0.9), as Doubles
val discounted = prices.map { it * 0.9 }
println(discounted)
// 3) Sum the original prices using fold starting at 0
val total = prices.fold(0) { acc, p -> acc + p }
println("Total: $total") // expect Total: 95
// ✅ First line: [25, 40, 15]
}Mini-Challenge: A Sales Report
Build one pipeline that filters, maps, and folds to produce a single total.
// 🎯 MINI-CHALLENGE: a sales report.
// Save as Sales.kt and run main().
//
// Given: val sales = listOf(120, 80, 200, 50, 175)
// 1. Use filter to keep sales >= 100.
// 2. Use map to add a 5 handling fee to each of those (it + 5).
// 3. Use fold (starting at 0) to total them.
// 4. Print "Big sales total (with fee): <total>".
//
// ✅ Example output:
// Big sales total (with fee): 510
fun main() {
val sales = listOf(120, 80, 200, 50, 175)
// your code here
}Common Errors (and the fix)
- Expecting map to mutate — map returns a NEW list; the original is unchanged. Capture the result in a variable.
- "reduce on empty collection" — reduce throws if the list is empty. Use fold with a start value instead.
- Using forEach to build a list — forEach returns nothing. Use map to produce a list.
- Wrong accumulator type — In fold, the start value's type must match what the lambda returns. Keep them consistent.
Pro Tips
- 💡 sumOf / maxByOrNull often beat fold for common aggregations: list.sumOf { it.price }.
- 💡 mapNotNull maps and drops nulls in one step — great for parsing.
- 💡 asSequence() for huge data: it processes lazily, avoiding intermediate lists.
📋 Quick Reference — Collection Ops
| Operation | Kotlin Syntax |
|---|---|
| Transform | list.map { it * 2 } |
| Select | list.filter { it > 0 } |
| Aggregate (start) | list.fold(0) { a, n -> a + n } |
| Aggregate | list.reduce { a, n -> a + n } |
| Group | list.groupBy { it.type } |
| Sum a property | list.sumOf { it.price } |
🎉 Lesson Complete!
- ✅ map transforms; filter selects
- ✅ Operations chain into readable pipelines
- ✅ fold aggregates with a start value; reduce starts from the first element
- ✅ groupBy partitions into a Map
- ✅ These operations never mutate the original collection
Practice quiz
What does map do to a collection?
- Removes elements that fail a test
- Mutates the original list
- Transforms each element and returns a new list
- Sums the elements
Answer: Transforms each element and returns a new list. map applies a lambda to every element and returns a new list.
What does filter return?
- A new list of elements where the lambda returns true
- A single boolean
- The original list reversed
- The count of matches
Answer: A new list of elements where the lambda returns true. filter keeps only elements for which the predicate is true.
How does fold differ from reduce?
- fold mutates the list; reduce does not
- They are identical
- reduce takes a seed value
- fold takes an explicit initial value; reduce starts from the first element
Answer: fold takes an explicit initial value; reduce starts from the first element. fold has an initial accumulator and can change type; reduce starts from element one.
What does reduce do on an empty collection?
- It returns 0
- It throws an exception
- It returns null
- It returns an empty list
Answer: It throws an exception. reduce throws on empty because there is no first element; fold returns its seed.
What does listOf(1,2,3,4).fold(0) { acc, n -> acc + n } evaluate to?
- 10
- 24
- 0
- 1234
Answer: 10. It sums starting from 0: 0+1+2+3+4 = 10.
What does groupBy produce?
- A single combined value
- A sorted list
- A Map keyed by the lambda's result, each value a list of members
- A boolean
Answer: A Map keyed by the lambda's result, each value a list of members. groupBy partitions elements into a Map<K, List<V>>.
Do map, filter, and fold modify the original collection?
- Yes, they mutate in place
- No, they return a new collection or value
- Only fold mutates
- Only on mutable lists
Answer: No, they return a new collection or value. These operations are non-destructive; the original is untouched.
What is the difference between map and forEach?
- forEach returns a new list; map does not
- They are the same
- map runs side effects only
- map returns a new collection; forEach returns nothing useful (Unit)
Answer: map returns a new collection; forEach returns nothing useful (Unit). Use map to produce data; forEach for side effects like printing.
What does nums.first { it > 3 } return for listOf(1,2,3,4,5,6)?
- true
- 4
- [4, 5, 6]
- 6
Answer: 4. first returns the first element matching the predicate, which is 4.
How can you make a long map/filter chain lazy to avoid intermediate lists?
- Use forEach
- Use a mutable list
- Call asSequence() first
- Wrap it in fold
Answer: Call asSequence() first. asSequence() processes each element through the chain lazily.
Continue this course
- Previous: Lambdas & Higher-Order Functions
- Next: Classes & Constructors — Model real-world things with classes, primary and secondary constructors
- Quick reference: Kotlin cheat sheet › Collections
Frequently asked questions
What is the difference between map and forEach?
map transforms each element and returns a brand-new collection of the results — it is for producing data. forEach runs a side-effecting action (like printing) for each element and returns nothing useful (Unit) — it is for doing something. If you want a new list, use map; if you just want to act on each item, use forEach.
How does fold work, and how is it different from reduce?
fold walks through a collection carrying an accumulator. You give it an initial value and a lambda (acc, element) -> newAcc; the lambda's result becomes the accumulator for the next step. reduce is the same idea but it uses the first element as the initial accumulator and starts from the second element — which means reduce throws on an empty collection, while fold safely returns its initial value. Use fold when you need a starting value or a different result type; use reduce for non-empty collections where the first element is a natural start.
Do these operations modify the original collection?
No. map, filter, fold, sorted, and friends are non-destructive: they return a new collection (or a value) and leave the original untouched. This makes chained pipelines safe and predictable. If you specifically want to mutate a list in place, there are different functions such as sortBy or removeAll on mutable collections.
Are long chains of map/filter efficient?
For typical collection sizes they are perfectly fine and very readable. Each step does create an intermediate list, though. If you are processing a very large collection or many steps, you can call asSequence() first to switch to lazy evaluation, which processes each element through the whole chain one at a time without building intermediate lists. For most everyday work, plain chained operations are the right, clear choice.