Kotlin collections and higher-order functions

Filter, map, group, sort, and fold lists with lambdas instead of loops.

Kotlin
data class Person(val name: String, val age: Int, val city: String)

fun main() {
    val people = listOf(
        Person("Ada", 36, "London"),
        Person("Bo", 25, "Paris"),
        Person("Cy", 41, "London"),
        Person("Di", 25, "Paris"),
    )

    println(people.filter { it.age > 30 }.map { it.name })
    println(people.groupBy { it.city }.mapValues { (_, list) -> list.size })
    println(people.sumOf { it.age })
    println(people.sortedWith(compareBy({ it.age }, { it.name })).map { it.name })
    println(people.maxByOrNull { it.age }?.name)
    println(people.partition { it.city == "London" }.let { (london, others) -> london.size to others.size })
    println((1..5).fold(1) { product, n -> product * n })
}
Output
[Ada, Cy]
{London=2, Paris=2}
127
[Bo, Di, Ada, Cy]
Cy
(2, 2)
120

Explanation

Kotlin's standard library offers many functions that take a lambda and work on a whole collection. filter keeps matching elements, map transforms each element, and groupBy splits a list into a map by a key. sumOf adds up a computed value, partition splits a list into two by a condition, and fold combines all elements into one value starting from an initial value. Inside a lambda with one parameter, the implicit name it refers to the element.

These functions return new collections and leave the original unchanged. sortedWith(compareBy(...)) sorts by several keys, with later keys breaking ties, and the sort is stable. Chaining operations produces a short, readable pipeline, though each step creates a new list, so for very large data Sequences evaluate lazily to avoid the intermediate lists.

Explore the dry run
Guided walkthroughReady
1data class Person(val name: String, val age: Int, val city: String)
2
3fun main() {
4 val people = listOf(
5 Person("Ada", 36, "London"),
6 Person("Bo", 25, "Paris"),
7 Person("Cy", 41, "London"),
8 Person("Di", 25, "Paris"),
9 )
10
11 println(people.filter { it.age > 30 }.map { it.name })
12 println(people.groupBy { it.city }.mapValues { (_, list) -> list.size })
13 println(people.sumOf { it.age })
14 println(people.sortedWith(compareBy({ it.age }, { it.name })).map { it.name })
15 println(people.maxByOrNull { it.age }?.name)
16 println(people.partition { it.city == "London" }.let { (london, others) -> london.size to others.size })
17 println((1..5).fold(1) { product, n -> product * n })
18}

Follow the example

Start to inspect the source, values, and output. These prepared steps explain the published sample; grouped loop steps describe multiple iterations.

View the complete dry-run table
Prepared trace of the published program. Some rows group repeated iterations.
Step / lineWhat happensState after stepOutput so far
Keep people older than 30, then take their names.—
[Ada, Cy]
Group by city and replace each group by its size.—
[Ada, Cy]
{London=2, Paris=2}
36 + 25 + 41 + 25.—
[Ada, Cy]
{London=2, Paris=2}
127
Sort by age and break ties by name.—
[Ada, Cy]
{London=2, Paris=2}
127
[Bo, Di, Ada, Cy]
The oldest person; maxByOrNull returns null for an empty list, so ?. is needed.—
[Ada, Cy]
{London=2, Paris=2}
127
[Bo, Di, Ada, Cy]
Cy
partition returns a pair of lists: those that match and those that do not.—
[Ada, Cy]
{London=2, Paris=2}
127
[Bo, Di, Ada, Cy]
Cy
(2, 2)
fold starts at 1 and multiplies: 5 factorial.—
[Ada, Cy]
{London=2, Paris=2}
127
[Bo, Di, Ada, Cy]
Cy
(2, 2)
120

Cost and common mistakes

Each pipeline step is O(n); sorting is O(n log n).

  • maxByOrNull is the safe version. maxBy (older versions) throws on an empty collection.
  • Long chains create intermediate lists. For large inputs start with asSequence() to process one element at a time.
  • groupBy keeps the original order of elements inside each group.

Try it yourself

How would you list the names sorted by age descending?

Reveal answer and reasoning

people.sortedByDescending { it.age }.map { it.name }.

Further reading