forEachはつらいよ
記事の目的
- 世の中で言われている「forEachでなく、なるべくmapを使いましょう」という言説に対する解釈の言語化
- 自戒 (今後自分がバッドプラクティスを盛り込んだコードを書かないように...)
forEachとmapの違い
JavaScriptでは、配列に対する操作でforEachメソッドとmapメソッドが存在します。
いずれのメソッドも、配列の各要素に対して、引数に与えた関数の処理を行うという点は同じです。
この2つのメソッドの違いは非常にシンプルで、以下の通りです。
- forEachメソッド
- 戻り値が存在しない (いわゆるvoid)
- mapメソッド
- 戻り値が存在する
- 引数に与えた関数の戻り値の配列を返す
よく見るコード
以下のようなコードに遭遇することがあります。
let arr = [];
datas.forEach((e) => {
let data = {};
// xxxの場合はfield1に"hoge"を表示する
if (datas.hogeFlag1) {
data.field1 = e.field1 + "hoge";
}
if (datas.hogeFlag2) {
data.field2 = e.field2 + "fuga";
}
// 処理が50行ぐらいあるとする
arr.push(data);
});
doBulkInsert(arr);
上記は良くない例です。こういう場合はmapを使うべきです。
const arr = datas.forEach((e) => {
let data = {};
// xxxの場合はfield1に"hoge"を表示する
if (datas.hogeFlag1) {
data.field1 = e.field1 + "hoge";
}
if (datas.hogeFlag2) {
data.field2 = e.field2 + "fuga";
}
// 処理が50行ぐらいあるとする
return data;
});
doBulkInsert(arr);
あまり変わっていませんが、こちらの方がコードの読み手としては嬉しいです。
forEachでなくmapだと何が嬉しい?
forEachメソッドは、戻り値がありません。
そのため、コードを書いた人は必ず引数に与えた関数内で副作用のある処理をしています。
これはどういうことかというと、forEachメソッドが出現した時点で、読み手は引数に与えた関数内で何をしているかを1度確認する必要があります。
一方mapメソッドでは、配列を返します。
そのため、コードを読む人は、配列の中身に関心が無ければ関数内の処理を全て読む必要はありません。 逆に言えば、変数arrの内容に関心がある場合は、その中を読めばよいだけです。[1]
この差はデバッグにおいて非常に重要になってきます。
自分が開発に関わっているシステムで不具合が発生し、デバッグの必要に迫られたときに、不具合の発生個所と思われるコードの周辺を隅々まで読むでしょうか?
それも1つの方法かと思いますが、それでは時間がいくらあっても足りません。
前述のようにmapメソッドを使用していれば、読み手は変数arrへの関心の有無によって、該当のコードを読む/読まないを判断することが出来ます。この事は、読み手の脳のワーキングメモリの節約になり、非常に効果的だと考えます。
配列の操作って大体の言語で出来るよね
Pythonでは同名の同じことが出来るメソッドが存在します。
JavaではStream APIを利用すれば、Listに対して同名の同じことが出来るメソッドが利用できます。
配列という概念は多くのプログラム言語で存在するため、基本的な考え方は同じです。
あとがき
先人が書いた様々なコードを批判したい気持ちは勿論ありません。誰しも、自らの経験不足によって読み手に伝わりづらいソースコードを書いてしまった、という経験があると思います。過去に戻れるなら「自分が書いたあのコード、書き直したいな...」というものがいくつも浮かびます。
処理の流れを理解するのに、いちいち上から下まで全部読んでいたらキリがありません。その必要がある場合は、それは良くないコードだと言えると思います。
大事なのは、以下のことを忘れないことだと考えています。
- 「良いコード」を参考にして、その要素を取り入れる
- 読み手が隅々までコードを読まなくてもいいようにしてあげるよう心掛ける
- 関数に関心事を個別のブロックスコープに閉じ込めるような意識が大事
-
ただし、mapメソッドの引数に与えた関数内で副作用のある処理が行われている場合は、話は別です。そんなコードがあったら頭を抱えるかと思いますが、その場合は諦めてコードと向き合いましょう ↩︎
Discussion