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 likefor
orif
clearly highlight these side effects.forEach
, however, masks imperative code with a veneer of functional style. It’s only marginally more concise than afor
loop, yet it often discourages developers from exploring whether the code could be genuinely functional. For instance, I frequently seeforEach
loops used for adding elements into a mutable lists, instead of just usingmap
.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’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..?)