JavaRush /Courses /Kotlin SELF /Common pipeline mistakes

Common pipeline mistakes

Kotlin SELF
Level 26, Lesson 3
Available

1. Why pipelines break silently

Pipelines on collections look nice: a chain map → filter → group → sort → take reads almost like a sentence in Russian. But that is exactly why mistakes in them are tricky: the program can compile successfully and even print something—just not what you intended. Today we will practice recognizing these “silent” breakages before they grow into “why does the report show zero when I definitely spent money.”

Let’s lock in the basic picture: a pipeline is a sequence of data-shape transformations, and at every step it is important to remember what exactly gets returned.

flowchart TD
    A["List⟨Pair⟨String, Int⟩⟩ (expenses)"] --> B["Normalization (map)"]
    B --> C["Filtering (filter)"]
    C --> D["Aggregation (groupBy+fold / groupingBy)"]
    D --> E["Sorting (sortedByDescending)"]
    E --> F["Selection (take)"]
    F --> G["Text rendering (StringBuilder)"]

If in this chain you “lost” the result of a step, confused an operation that “returns a new list” with an operation that “mutates in place,” or hid a side effect inside map, then the pipeline turns into a beautiful, but very confident lie.

2. Lost the result of sorted/filter/map

This mistake looks so innocent that you want to justify it: “well, I called sort—so the list got sorted.” But many collection operations in Kotlin do not mutate the source, and instead return a new result. For example, sorted() creates a new list, unlike sort() on a MutableList, which sorts in place. That is why you often have function pairs: “in-place” and “returns a new collection,” for example sort() vs sorted().

Example: why didn’t the list get sorted?

fun main() {
    val amounts = listOf(30, 10, 20)

    amounts.sorted() // result lost
    println(amounts) // [30, 10, 20]  (nothing not changed')
}

Fix—store the result:

fun main() {
    val amounts = listOf(30, 10, 20)

    val sorted = amounts.sorted()
    println(sorted)  // [10, 20, 30]
    println(amounts) // [30, 10, 20]  (source lives its own life)
}

Mini table: mutates or returns?

It is convenient to build intuition with a small cheat sheet (not rote memorization, but “so your eye gets used to it”). For collections in Kotlin, the idea “there is a pair of functions” is normal practice.

What we want to do If the collection is read-only (List) If the collection is mutable (MutableList)
Sort sorted() returns a new list sort() sorts “in place”
Filter filter() returns a new list usually filter() also returns a new one; “in place” filtering is done differently, but that’s a separate story
Transform elements map() returns a new list map() also returns a new one

In our practical app (expense tracking) this shows up very often: you build a report and then “slightly sort the expenses before output” —and suddenly nothing changes, because the sorting result was not saved.

3. map for side effects and a Unit list

This mistake appears when your brain thinks: “I need to iterate over a list and do something,” and your hand automatically reaches for map. But semantically, map means “transform each element into a new element and collect a new list.” If inside you do println, then the result of map becomes a list of Unit. And Kotlin honestly gives it to you.

Example: anti-pattern

fun main() {
    val categories = listOf("food", "taxi")

    val result = categories.map { println(it) } // print = side effect
    // food
    // taxi

    println(result) // [kotlin.Unit, kotlin.Unit]
}

Sometimes students look at [kotlin.Unit, kotlin.Unit] and think it is some new collection of mysterious units that must be defeated with magic. In reality, it is simpler: println(...) returns Unit, so map collected “results”—that is, Unit.

Fix: if you are doing an action rather than building a new collection

The most straightforward path is a plain for:

fun main() {
    val categories = listOf("food", "taxi")

    for (c in categories) {
        println(c) // food, taxi
    }
}

You can also use forEach, but in our course it is often easier to start with for, because it does not hide magic. The main idea: map is not “to iterate,” it is “to transform.”

In reports this is especially important: if you print something for debugging “inside the pipeline,” you get side effects that are then hard to remove, and even harder to understand where exactly they occur.

4. groupBy for statistics and when eachCount is better

When you need statistics like “frequencies” or “how many times a value occurred,” many people habitually do groupBy, because it is familiar. But groupBy returns Map<K, List<V>>: for each key it builds a list of elements in the group. This is fine if you actually want to work with the group list afterward.

But if you only need statistics (for example, “how many expenses in each category” or “how often a word occurs”), then those group lists become unnecessary baggage.

Example: category frequencies via groupBy (works, but a bit heavy)

fun main() {
    val categories = listOf("food", "taxi", "food")

    val grouped = categories.groupBy { it } // Map<String, List<String>>
    val counts = grouped.mapValues { (_, group) -> group.size }

    println(counts) // {food=2, taxi=1}
}

Example: the same via groupingBy().eachCount()

groupingBy() gives a Grouping object, and eachCount() counts elements by groups without building group lists.

fun main() {
    val categories = listOf("food", "taxi", "food")

    val counts = categories.groupingBy { it }.eachCount()
    println(counts) // {food=2, taxi=1}
}

For our expense app this means: if you are building a report like “how many operations per category,” then eachCount() is not only shorter, but also closer to the meaning of the task.

5. Two sorts in a row and “magical” chains

There is a special class of “mistakes” where the result is correct, but the code does extra work and becomes hard to read. Sorting is a common example: you sorted by amount, then again by name, then took take(5), then sorted again, because “it looks kind of off.”

The problem here is not only performance—it is that the meaning starts to blur: it is unclear which rule is ultimately the main one.

Let’s look at a typical situation: we have totals by category, and we want top-N. A good idea: sorting should happen once, closer to the “presentation” (i.e., output), rather than being smeared across all the logic.

Example: questionable chain

fun main() {
    val totals = listOf("taxi" to 250, "food" to 200, "books" to 90)

    val top = totals
        .sortedBy { it.first }            // sort by name
        .sortedByDescending { it.second } // and then by sum (overwriting idea)
        .take(2)

    println(top) // [(taxi, 250), (food, 200)] (might accidentally coincide, but logic unclear)
}

Yes, Kotlin sorts are stable (we are not diving into internals here), but even if the result is “fine,” the code does a poor job of explaining intent.

Example: clearer—one sorting rule

fun main() {
    val totals = listOf("taxi" to 250, "food" to 200, "books" to 90)

    val top = totals
        .sortedByDescending { it.second } // main criterion
        .take(2)

    println(top) // [(taxi, 250), (food, 200)]
}

If you need “by amount descending, and when equal—by name,” then it is better to express this as a single criterion (via sortedWith + compareBy). But what matters more here is catching the smell of the problem: “the chain looks like magic.”

6. Extra passes over the data

This mistake is not always noticeable on small lists (and on small lists almost everything runs fast). But in reports it shows up unpleasantly: you write “nicely,” and then a report on 100,000 operations starts to lag, and you do not understand why.

A common cause is multiple independent passes that could have been combined, or at least made more intentional. For example, you first compute filtered, then separately count, then separately sum, and each time the filtering is repeated.

Example: repeating the same filter twice

fun main() {
    val amounts = listOf(10, 200, 30, 500)

    val countBig = amounts.filter { it >= 100 }.count()
    val sumBig = amounts.filter { it >= 100 }.fold(0) { acc, x -> acc + x }

    println(countBig) // 2
    println(sumBig)   // 700
}

Does it work? Yes. Does it do two passes with the same filter? Also yes.

In real life you do not always have to “optimize,” but you do have to understand what is happening. Often it is enough to pull the filtered result into a val, and your code becomes both faster and more readable.

Fix: one filter—two computations

fun main() {
    val amounts = listOf(10, 200, 30, 500)

    val big = amounts.filter { it >= 100 }
    val countBig = big.count()
    val sumBig = big.fold(0) { acc, x -> acc + x }

    println(countBig) // 2
    println(sumBig)   // 700
}

In reporting functions this is especially appropriate: intermediate val values give you “stopping points” where you can visually check what data you have after a step.

7. Sequence: laziness and a forgotten terminal step

With Sequence the classic story happens: you read that it is “faster,” you enable asSequence()—and then mysticism begins. In reality there is no mysticism: Sequence is lazy. Intermediate operations (map, filter) only describe the steps, and actual computation starts at a terminal operation—for example, toList().

Example: I thought I already computed it

fun main() {
    val xs = listOf(1, 2, 3)

    val seq = xs.asSequence()
        .map { it * 10 }

    println(seq) // kotlin.sequences.TransformingSequence@... (not a list!)
}

This is not a bug. You printed the pipeline description object, not the result.

Fix: materialize the result

fun main() {
    val xs = listOf(1, 2, 3)

    val ys = xs.asSequence()
        .map { it * 10 }
        .toList()

    println(ys) // [10, 20, 30]
}

Important nuance: side effects inside Sequence run later

If inside map you have println, then you do not control the moment of printing as obviously as with a normal list. Printing will only happen when a terminal operation occurs.

fun main() {
    val xs = listOf(1, 2, 3)

    val seq = xs.asSequence()
        .map {
            println("mapping $it") // execute later
            it * 10
        }

    println("Before toList()") // Before toList()
    val ys = seq.toList()
    println("After toList()")  // After toList()
    println(ys)                // [10, 20, 30]
}

This effect is useful when debugging, but harmful if you accidentally mixed computation and output.

8. Map in a report and unstable order

When you build a report, it is very tempting to do println(totalsByCategory) and be happy. But in a real report something else matters: output must be predictable. Even if you are not writing tests, a person (or you a week later) wants to see the same line order every time.

From a type perspective everything is logical: Map is not a “list of lines,” it has its own nature. When you need ordering, you usually turn entries into a list and sort explicitly.

Example: rendering a category report with explicit sorting

fun renderTotalsByCategory(totals: Map<String, Int>): String {
    val rows = totals.entries
        .map { it.key to it.value }
        .sortedBy { (cat, _) -> cat } // sort by name category

    val sb = StringBuilder()
    for ((cat, sum) in rows) {
        sb.append(cat).append(": ").append(sum).append('\n')
    }
    return sb.toString()
}

And a small check:

fun main() {
    val totals = mapOf("taxi" to 250, "food" to 200, "books" to 90)
    print(renderTotalsByCategory(totals))
    // books: 90
    // food: 200
    // taxi: 250
}

In this code we separate “data” and “presentation”: first we build ordered data rows (a list of pairs), and then we assemble the text. This helps not only with order, but also with readability.

9. Mini breakdown: expense report and typical failures

Now we will take our familiar day data format: a list of expenses List<Pair<String, Int>>, where first is the category, second is the amount. What matters is not “writing the perfect report,” but seeing how small mistakes in the pipeline turn the result into something wrong or strange. This is the kind of practice where the brain stops trusting words and starts trusting types and return values.

Source data

fun main() {
    val expenses = listOf(
        " food " to 120,
        "FOOD" to 80,
        "taxi" to 250,
        "books" to 90
    )

    println(expenses)
    // [( food , 120), (FOOD, 80), (taxi, 250), (books, 90)]
}

Error #1: forgot normalization before grouping

fun main() {
    val expenses = listOf(" food " to 120, "FOOD" to 80)

    val totals = expenses
        .groupBy { it.first } // " food " and "FOOD" become different keys
        .mapValues { (_, items) -> items.fold(0) { acc, x -> acc + x.second } }

    println(totals) // { food =120, FOOD=80} (the category is “split”)
}

Why? Because groupBy() builds keys exactly from what you give it.

Fix: normalize the key before grouping

fun normCategory(s: String): String = s.trim().lowercase()

fun main() {
    val expenses = listOf(" food " to 120, "FOOD" to 80)

    val totals = expenses
        .map { (cat, amount) -> normCategory(cat) to amount }
        .groupBy { it.first }
        .mapValues { (_, items) -> items.fold(0) { acc, x -> acc + x.second } }

    println(totals) // {food=200}
}

Error #2: sorted, but lost the result

fun main() {
    val totals = listOf("taxi" to 250, "food" to 200, "books" to 90)

    totals.sortedByDescending { it.second } // lost result
    println(totals) // [(taxi, 250), (food, 200), (books, 90)] (as it was)
}

It feels like “it is already sorted anyway” while the data happens to be in that order. But this is a trap: sortedByDescending returns a new list, and does not change the original.

Fix: keep the result

fun main() {
    val totals = listOf("taxi" to 250, "food" to 200, "books" to 90)

    val sorted = totals.sortedByDescending { it.second }
    println(sorted) // [(taxi, 250), (food, 200), (books, 90)]
}

10. Common pipeline mistakes

Mistake #1: called sorted()/filter()/map() and did not store the result.
This happens because many operations return a new collection rather than mutate the old one. The cure is a habit: if the operation is not “in-place,” then you almost always need a val. Hint: MutableList has sort(), while List has sorted().

Mistake #2: using map as “just iterate and do an action.”
If inside map you have println, writing to an external var, or something similar, then you mixed transformation with a side effect. The result either gets lost, or turns into a list of Unit. Usually it helps to replace it with for and, if needed, separate computation from output.

Mistake #3: using groupBy where you only need statistics.
groupBy creates a Map<K, List<V>>. If afterward you only take size or just count frequencies, you built unnecessary lists. It is often simpler and more honest (semantically) to use groupingBy().eachCount().

Mistake #4: multiple sorts in a row “to be safe.”
The code starts to look like a “ritual”: it seems to work, but it is unclear why. It is usually better to formulate one sorting rule (or one composite rule), apply it once, and only then do take(n).

Mistake #5: enabled Sequence but forgot the terminal step.
Intermediate operations on Sequence do not run by themselves. Until toList() (or another terminal step) happens, you are holding a “computation plan,” not a result. And if there are side effects inside, they will fire later—at the moment the result is materialized into a collection.

Mistake #6: the report prints a Map without explicit sorting and produces an “unstable” line order.
When you build a report for a person, output order is part of the contract. If you need order, turn entries into a list of pairs and sort explicitly, then render the text. This makes the result deterministic and calm.

1
Task
Kotlin SELF, level 26, lesson 3
Locked
Score ordering
Score ordering
1
Task
Kotlin SELF, level 26, lesson 3
Locked
Chat messages
Chat messages
1
Task
Kotlin SELF, level 26, lesson 3
Locked
Label frequencies
Label frequencies
1
Task
Kotlin SELF, level 26, lesson 3
Locked
Stable report
Stable report
Comments
TO VIEW ALL COMMENTS OR TO MAKE A COMMENT,
GO TO FULL VERSION