Kotlin の `forEach` は可読性を下げる
Kotlin でコレクションの要素を処理するとき、forEach
関数は最後の手段としては使用をなるべく避けた方が賢明です。
確かに list.forEach { ... }
という書き方は、ラムダ式で少ない文字数で処理を記述できます。しかし一方で、「forEach
は命令的なコード(サイドエフェクトを伴うコード)なのか、それとも関数的なコード(データ変換を意図するコード)なのか」が曖昧になり、可読性を下げる可能性があります。
本記事では、この問題と、forEach
の代替案について述べていきます。
forEach
が持つ曖昧さとは
Kotlin のコレクション操作には、大きく分けて以下の 2 つのスタイルがあります。
-
命令的スタイル (Imperative)
- 例:
for
文やwhile
文。繰り返し処理とサイドエフェクトを組み合わせるケースが多い。
- 例:
-
関数的スタイル (Functional)
- 例:
map
,filter
,fold
といった高階関数。コレクションを変換して新しいコレクションや値を返す。
- 例:
forEach
は「各要素に対してラムダ式を実行する」という高階関数ですが、返り値は Unit
であり、結果を返さずに要素を "使い切る" ために設計されています。そのため、副作用(サイドエフェクト) を伴う処理に利用されるという特徴があります。
命令的か、関数的か
val list = listOf("Alice", "Bob", "Charlie")
list.forEach {
println("Hello, $it!")
}
上記のように、コードはラムダ式で処理を記述しており、一見すると関数的スタイルに見えるかもしれません。しかし実際には、標準出力への書き込みというサイドエフェクトを行っている、非常に命令的な処理です。ここが「forEach
は本当に関数的な操作なのか、単に命令的な操作を高階関数っぽく書いているだけなのか」という曖昧さの原因です。
可読性を下げると言われる理由
1. “動き” を期待するか、結果を期待するかの違いが不明瞭
forEach
には返り値がなく、要素の加工や変換の意図が見えにくいため、コードを見る側が「この関数呼び出しはデータを変換しようとしているのか、それとも副作用を起こしたいだけなのか」を判断しにくくなります。チームでコードを共有するとき、この曖昧さが大きな混乱につながることがあります。
2. 関数型スタイルなのに副作用が散らばる
Kotlin は map
や filter
などを使うことで関数型スタイルのコードを書きやすい言語です。しかし、forEach
を多用すると、副作用的な処理がラムダ式に書かれ、結果としてコードのあちこちに副作用が分散する可能性があります。関数的なスタイルと命令的なスタイルが混在することで、後からコードを読むときに理解しづらい状況が生まれやすいのです。
3. 他のループ構文や高階関数との整合性
「単にループが必要なら for
文で十分」「データを変換したいなら map
や filter
を使う方が意図が明確」など、意図に応じた書き方を選ぶほうが分かりやすくなります。forEach
はその境界があいまいになりやすいため、チーム内のスタイルガイドなどで利用方針を明確にする必要があるでしょう。
代替案
forEach
は曖昧だからといって、必ずしも使ってはいけないというわけではありません。使う目的と意図を明確にすれば問題にならないケースも多々あります。以下にいくつかの代替案を紹介します。
for
文を使う
1. 素直に 副作用や命令的処理が主目的であれば、通常の for
文を使うのが最も明確で分かりやすい方法です。
for (name in list) {
println("Hello, $name!")
}
命令的な処理をしていることがコード上で一目瞭然なため、読んだときの混乱を防ぐ効果があります。
map
・filter
・fold
などを使う
2. 新しいコレクションを作りたい、あるいは集計したいといったようにデータの変換が目的の場合は、map
や filter
, fold
といった高階関数を使うのが正攻法です。
val uppercaseNames = list.map { it.toUpperCase() }
val filteredNames = list.filter { it.length > 3 }
val joinedNames = list.fold("") { acc, name -> "$acc,$name" }.removePrefix(",")
これらの関数は、変換・絞り込み・畳み込みなど、明確な処理意図と結果の受け渡しがあるため、関数的なスタイルと非常に相性が良いと言えます。
onEach
を活用する
3. Kotlin 1.1 で追加された onEach
は、forEach
によく似た関数ですが、副作用を行いつつ元のコレクションをそのまま返す という点が大きく異なります。これにより、副作用とメソッドチェーンでの処理を同時に行いたい場合に便利です。
val result = listOf("Alice", "Bob", "Charlie")
.onEach { println("DEBUG: $it") } // ログ出力などの副作用
.map { it.toUpperCase() } // 変換
.filter { it.length > 3 } // 絞り込み
onEach
ならチェーンの途中で副作用的な処理を挟みつつ、コレクションを返せるため、命令的・関数的スタイルを共存させやすいです。
まとめ
同じ結果を得られる書き方であっても、コードの意図をよりよく表出する書き方を選ぶことができれば、コードの可読性を改善することができます。機械的な側面だけではなく、より優れたコードを目指したいものです。
Discussion