LoginSignup
1
2

More than 3 years have passed since last update.

forを避ける理由とmap, filter, reduceなど

Last updated at Posted at 2020-12-13

はじめに

何番煎じの話か分からないですが、for(従来のfor文や同様の問題を持つ拡張forやforEach)を避けるべき理由と代替手段を説明していきます。
以下の内容は私がWeb業界で主にAPIサーバのビジネスロジック部分のコードを書いてきた経験から書いており、一部の分野に偏った話になってしまっています(世の中の全てのforを批判する意図はありません)。
やや主語デカタイトルになってしまっていてごめんなさいしておきます。

コード例は主にJavaで書いていますが、内容自体は他の言語で記述した場合にも当てはまりそうなことをまとめています。

forを避けるべき理由

まず昔ながらのfor文を記述します。
よく知られている以下の構文です。

for(int i = 0; i < array.length(); ++i){
  ...
}

この構文はC言語、C++言語、Java、C#、JavaScriptなどなど様々な言語に存在します(型や配列のサイズなど細かな部分は異なる)。

この構文には、保守性や可読性[^1]に対して以下の問題点があります。

  1. ループに使う添え字をミスしてバグりやすい
  2. 記述の自由度が高い
    1. forの外にある変数への書き込みが必要
    2. 行いたいことが複数書けてしまう
    3. continue/break の扱いは難しい

それぞれの内容について以下で解説していきます。

1. ループに使う添え字をミスしてバグりやすい

これは従来のfor文で発生する問題です。
この問題を引き起こす例としては上のコードだとi < array.lengthi <= array.lengthと書いてしまうことです。
また、ループを二重にした場合に内側のループと外側のループの添え字を使い間違えることもあります。
実際for文を書いた方はこの種の書き間違いを間違いなく経験していると思います。

この問題点についてはJavaScriptならfor of、Javaなら拡張for(for (var item : array))などを使うと解消できます。
類似の構文は近いパラダイムの新しめのプログラミング言語にはだいたいあると思います(あっC言語の標準機能には…)。

2. 記述の自由度が高い

そもそもforは記述の自由度が高いです。[^2]
「ループの中で複数の互いにあまり関連していない処理を記述する」
「ループの途中で抜けて中断した段階まで要素が追加されたリストを使用すること」[^3]
など、少々変則的な処理でも比較的簡単に書けてしまいます。
この何でもできてしまうところが悪い面として出て保守性や可読性を下げるということにつながると私は思っています。
それぞれの問題点について書いていきます。

2.1 forの外にある変数への書き込みが必要

ループ内で何かしらの処理を行うわけなので、通常はその処理結果を何かしらの形で出力します。
画面への入出力などの変数ではないIOを考慮外とすると、出力としてはループの前に定義した変数を書き換えることになります。
つまり、以下のようなコードになります。

var output = new ArrayList<ExampleData>();
// ※1
for (var item: inputList){
  output.add(new ExampleData(item));
}
// ※2

問題点としては、outputに対して再代入や変更が許されることです。
つまり、以下を気にする必要が出てきます。

  • forの前に書いた処理(※1の箇所)に記述された別の処理の結果がoutputに入っている
    • メソッドの外からoutputが渡されるケースも考えられる
  • forの後に書いた処理(※2の箇所)でoutputが書き換わる

つまり、outputについてforの前後を見直す必要が出てしまいます。

処理がこのforのみで切り出されていればいいのですが、
複数人で開発しているときにはこのように書けること自体が好ましくないと考えています。
同じ変数が書き換えられて使いまわされることは保守性や可読性を下げると思います。

もう一つの問題としては実装者への教育的影響が挙げられます。
forを使うとforの外にある変数に書き込む、というデータの流れが通常となるので、
入力と出力が曖昧なコードを書くクセがついてしまう可能性があります。

後述のfiltermapなどを使うと、処理を配列(やリスト)の要素ごとの単位での入出力として記述することをある程度強制[^4]できます。

2.2. 行いたいことが複数書けてしまう

まず、コード例です。

var filteredUserIdList = new ArrayList<Id>();
var existUserError = false;
for (var user : userList){
  if(user.hasError()){
    existUserError  = true;
  }

  if(filterMatch(user)){
    filteredUserIdList.add(user.id);
  }
}

このコードでは以下の結果をもとめようとしています。

  • ユーザーの一覧データの中に1人でもエラーを持つ人がいるかどうか(existUserError)
  • ユーザーの一覧データの中でフィルタリング処理(filterMatch)でマッチするユーザーID一覧

上記はぱっと思いついたものを書いたので優れた例ではないかもしれません。
この例は「同一の入力データuserListを使っている」ということ以外には互いにあまり関連のない処理が混在している、ということを伝えるつもりで書いています。

一度forで記述してしまうと、私の経験上はリファクタリングする強い意思を持った人がいない限り内部の処理が肥大化していくことが多かったです。
つまり、同じforの内部に多くの意図を持った処理が書かれ、巨大なforになっていきます。
保守性の観点からはforで書くにしても処理の意図に応じた複数のforに分割してほしい…というのが私の思いです[^5]。

2.3. continue/break の扱いは難しい

広く知られているように、continuebreak はループの制御に使用する文です。
forを記述していると、以下のような場面がよくあります。

  • 配列に1個でも異常な条件となる要素がある場合にループを終了し特別な処理に行きたい
  • 配列のある要素が不完全な場合は次の要素の処理に処理に行きたい

例として以下のコードを示します。

for (var item : list){
  if(item.hasError()){
    // エラーのある要素が存在→エラーメッセージを追加して抜ける
    errorList.add(new Error("error!"));
    break;
  }
  if(item.hasNoData()){
    // データがないときは次の要素の処理へ
    continue;
  }
  ...
}

これには以下の問題点があります。

  • continue/breakから直接に意図を読み取れない
    • 「配列に1個でも異常な条件となる要素がある場合」のような意図がコメントなしに伝わらない
  • continue/breakの処理順序を気にする必要がある
    • 例えば「配列のすべての要素に対して処理を行いたい」という場合に、continue/breakの下に処理を記述してしまった場合、記述した処理は配列のすべての要素に対して動く保証が無くなる

これらの問題点は2.2「行いたいことが複数書けてしまう」の問題と合わさり、
人間にとって処理を追う・意図通りに記述するという作業の難易度が高いコードになりやすいと思っています。

仕様がふんわりとしていて実装と同時並行で仕様が固まっていくような開発の場合、
「配列に1個でも異常があれば1つだけエラー出力」
「配列の要素の中で異常なものそれぞれについてエラー出力」
のような仕様が考えられるケースで
continueを使うべきか、breakを使うべきかという点を曖昧にしてしまい、
後々の技術的負債になることもあります。
言い換えると、実装者が仕様(意図していること)と実装の乖離に気づけない場合があるという問題点です。
今まで単体テストがなかった処理に単体テストを書いた時にこれが露見することがあります(ありました)。

forの代替になるもの(map, filter, reduceなど)

forを使って行いたいことは、状況によっては以下の処理を使うことで代替できます。
上述の問題をすべて解消できるわけではないですが、ある程度は改善できます。
JavaやJavaScriptでの名称で書いていますが、それ以外の言語にも類似のものはあります(メソッド名は異なる場合あり)。
※下に書いたものは一部で、他にも色々あります。

  • some(JavaScript) / anyMatch(Java) / noneMatch(Java)
    • 条件に合う要素が1つでも存在するかどうか(※noneMatchはその逆)
  • filter
    • 条件に⼀致する要素を取り出し
  • map
    • 要素を1対1で処理(計算や変換)
  • reduce
    • 要素を累積して単一の値をもとめる
    • 合計値算出に使う例がよく知られていますが、JavaScriptで{key: string, value: number}[]のような配列を単一のオブジェクトにまとめるような場合にも使えます。
  • その他
    • JavaScriptの場合Arrays.prototype.indexOfも置き換えの選択肢に入るかも

上記だけではピンとこない方もいると思うので一例を以下に示します。

①のコードは②に書き換えできます。


var filteredUserIdList = new ArrayList<Id>();
for (var user : userList){
  if(!filterMatch(user)){
    continue;
  }
  filteredUserIdList.add(user.id);
}


final var filteredUserIdList = userList.stream()
    .filter(u -> filterMatch(u))
    .map(u -> u.id)
    .collect(Collections.toUnmodifiableList());

上記の例で最初に挙げた問題点が完全とは言わないまでも解消したと感じていただければ幸いです。

ちなみに慣れてくると連続した処理をメソッドチェーンでスッキリ書けるというメリットもあります。

実務経験が浅めの人はforで書かなくても済む場面でもforを使いがちではないかと推測しています(おそらく過去の自分にも当てはまる)。
このあたりは実務に入る前にノウハウとして伝えておくと良いのかもしれません。

注意点

※置き換える場合、以下の注意点も気にしてください。

  • 処理の順序などに注意。単純に置き換えできない場合があります。
  • 当然ですがプログラミング言語それ自体と言語のバージョンなどで内部の動きや挙動が違うので仕様を調べてから使ってください。
    • Javaの場合、例外とStreamとの相性は良くないようなので、行う処理によっては考慮が必要です。

目的によっては避ける必要はない

再度になりますが、上記の内容は筆者が画面やAPIサーバのビジネスロジック部分のコードを書いてきた経験からの内容です。

Web業界においても性能(速度・メモリ制約)などのためにデータ構造を重視する処理を記述する場合(レイヤーの低いコード)はforを使う場面もあると思います。
また、組み込み環境など性能、環境、言語上の制約などからforで書くしか選択がないという場合もあります。
この記事では保守性や可読性を目的にforを避けたい、という内容にしていますが、forを避けるかどうかは目的次第です。

また、開発メンバーが意図を読めなくなってしまうとforを避けることのメリット以前に保守できなくなってしまうので、
あまりにも無理のある書き方で避けることもないのかなと私は思っています。

過去には組み込み業界にもいたので思うところですが、今後の言語や環境の進歩や淘汰によってforのバグりやすさと戦わずに済む業界が増えていけばいいですね。

[^1]: この記事での可読性とはコードの読み手(未来の自分、開発メンバー)に意図が伝わるという意味合いです。
[^2]: mapやfilterなどの処理でも悪意を持って書くと構文上は後述の行儀の悪いコードは書けます。自由度が高いの意味合いとしてはforを使うことを認めるとそれと同時に行儀の悪い状態を黙認することになりがちという意味合いです。
[^3]: たぶんある程度わかる人ならreduceかfilterで書けます。
[^4]: 記法的な強制のみ mapで外部の変数を書き換えるような行儀の悪いコードを見たことがあるのでコードレビューなどで食い止める必要はあります。
[^5]: 環境によってはパフォーマンスなどの制約で意図的に1つのforにする場合もあると思います。

1
2
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
1
2