Choosing Wisely: for vs. forEach in Kotlin (A Thinking Dev’s Perspective)

Choosing Wisely: for vs. forEach in Kotlin (A Thinking Dev’s Perspective)

The forEach loop, are you using it without thinking? In Kotlin, empirically forEach is often the go-to loop, it’s pragmatically close at reach in the IDE auto-complete, but is it always the right choice?

This post argues that for loops deserve more consideration, offering clarity and control that forEach sometimes lacks. Let’s discuss why a balanced & considered approach to looping is crucial for cleaner, more effective code.

Many Kotlin developers instinctively reach for forEach due to its concise syntax. However, this can lead to overlooking the strengths of the traditional for loop. This isn’t about declaring one “better” than the other; it’s about understanding what they represent and making an informed choice. The explicit nature of a for loop often provides valuable clarity and control that forEach can obscure.

The core argument against reflexively defaulting to forEach often touches on two key areas: situations where dedicated functional operators (map, filter, etc.) offer better clarity, and situations involving necessary side effects where forEach might mask the imperative nature of the code.

César Puerta eloquently captures the core argument against overusing forEach:

The core argument is that forEach is fundamentally an imperative construct that relies solely on side effects. When writing imperative code, using imperative statements like for or if clearly highlight these side effects. forEach, however, masks imperative code with a veneer of functional style. It’s only marginally more concise than a for loop, yet it often discourages developers from exploring whether the code could be genuinely functional. For instance, I frequently see forEach loops used for adding elements into a mutable lists, instead of just using map.

On a more subtle note, I believe it impairs readability. When I encounter chains of functional expressions, I naturally assume the functions are pure. This allows me to grasp the high-level structure of the computation by examining inputs and outputs. Imperative operators, by design, are not pure; they acquire inputs and outputs outside the explicit data flow. Consequently, to fully understand them, I must delve into their internal workings to map their dependencies. A programming model that prioritizes pure lambdas and uses explicit imperative constructs for non-pure computations facilitates understanding the overall code structure and data flows at a glance.

Ultimately, it comes down to readability and employing a consistent set of conventions to effectively convey the intent and structure of the computation.

César Puerta

César’s key point is that forEach, while seemingly functional, often masks imperative code relying on side effects. This hinders readability because it blends functional appearance with imperative behaviour. Even when side effects are necessary, using an explicitly imperative construct like for makes it immediately clear that the code is performing actions beyond simple data transformation, aiding comprehension.

He even takes a stronger stance than the balanced view presented here, adding: “Controversially to the balanced example in this blog, I am ready to argue that, except in the simplest cases, a for loop is superior to a forEach!

Let’s work through an example

Problematic forEach usage

Using forEach for filtering and transformation obscures the intent:

val numbers = listOf(1, 2, 3, 4)
val doubledEvens = mutableListOf<Int>()
numbers.forEach { number ->
    if (number % 2 == 0) {
        doubledEvens.add(number * 2)
    }
}
println(doubledEvens) // Output: [4, 8]

Improved for loop version

The for loop makes the conditional logic and list modification explicit:

val numbers = listOf(1, 2, 3, 4)
val doubledEvens = mutableListOf<Int>()
for (number in numbers) {
    if (number % 2 == 0) {
        doubledEvens.add(number * 2)
    }
}
println(doubledEvens) // Output: [4, 8]

Even better, functional approach

The functional approach with filter and map is even more concise and expressive when appropriate:

val numbers = listOf(1, 2, 3, 4)
val doubledEvens = numbers
        .filter { it % 2 == 0 }
        .map { it * 2 }
println(doubledEvens) // Output: [4, 8]

The forEach example, while functional in that it works, obscures the intent. It’s using forEach to perform a filtering and transformation operation, which is more naturally expressed with a for loop or even better with a functional approach like filter and map.

The for loop version makes the conditional logic and the list modification much more explicit. It clearly shows the intent: iterate through the numbers, check if they are even, and if so, add their doubled value to the new list.

The functional approach using filter and map is even more concise and expressive if the requirement is simply filtering and transforming the original list. This would be the preferred approach if the logic is as simple as the example shows. The for loop becomes more relevant when the logic within the loop becomes more complex and requires more explicit control flow.

Conclusion

To reaffirm the argument being made; the forEach version, while not wrong, is not the best choice in this scenario. The for loop (or the functional approach) more clearly and directly expresses the logic, making the code more readable and easier to understand.

In conclusion, the choice between for and forEach isn’t about superiority; it’s about context. By consciously considering clarity, control, and the complexity of your logic, you can write more expressive and maintainable Kotlin code. Don’t just default to forEach; give for the love it deserves—your codebase will thank you.

Happy coding!

I’d love to hear your thoughts. Connect with me on:

BlueSky @Blundell_apps (or Threads, or X..?)

Leave a Reply

Your email address will not be published. Required fields are marked *