なぜfor文は禁止なのか。

結論からいうと、可読性のためです。
for文は何かを「繰り返し処理をする」ことに使いますが、実際に求められるのは、「全ての要素に処理をする」とか、「母集団から選択して処理をする」こともおおくあります。

また、for文を用いることは、関数名にfuncという名前をつけるのと似ています。こんな関数名あっても、何をしてくれる関数かわからないだろう。と。
for文も同じで、繰り返すやるのはわかるが、何故繰り返すのか、どの要素に対して何をやるのかわからないだろう、だから禁止だ。ということです。異論は認めます。

これ以上の言語化が難しいので、サンプルコードを例示します。

サンプルコード

サンプルの命題は、

0から100未満の偶数のみを累計する。

とします。

禁止

var totalOfEvenNumberUnder100 = 0;
for (var i = 0; i < 100; i++) {
    if (i % 2 === 0) {
        totalOfEvenNumberUnder100 += i;
    }
}

命題に、「繰り返す」という文字がないのに、forで繰り返しています。手続き型に慣れたプログラマは、もう違和感を感じなくなってしまっているかもしれませんが、なぜ繰り返すのでしょうか。0から100未満の数字がほしいだけなのに。
また、最初に0で変数を初期化するのも命題にはありません。
さらには、偶数のみを取り出す処理と足す処理が交互に行われることになっています。偶数のみを累計する、という命題とは違う処理になってしまっています。

もちろん答えは一緒ですが、命題とは別の処理になってしまっているともいえます。

推奨

できるだけ処理に名前をつけていきます。
命題を「0から100未満」と、「偶数のみ」という処理と、「累計する」という処理に分解して名前をつけます。

const from0To100Array = Array.from(Array(100).keys());
const isEvenNumber = i => i % 2 === 0;
const addAll = (total, i) => total + i;
const totalOfEvenNumberUnder100 = from0To100Array.filter(isEvenNumber).reduce(addAll);

「0から100未満」がfrom0To100Array
「偶数のみ」がisEvenNumber
「累計する」がaddAllです。
最後に「0から100未満」から「偶数のみ」を取り出して「累計する」をしています。
この方が命題に即しているのではないでしょうか?

今回は、関数型に有利な命題を選択したような気もするのですが、実際にプログラムに求められる処理には、このような形式の命題も多くあります。

for文のような手続き型だけでなく、関数型でも処理を記述できるエンジニアになってみるのも良いと思います。
(あれ?for文禁止から随分トーンダウンしたじゃないか、というご指摘ですか?ですよね。。。実際for文を書いたほうが、短くプログラミングできることもありますからね。。。。失敬)

現場からは以上です。

249contribution

ぼくも関数型の考え方が大好きで多用しているので全体の趣旨としては賛成なんですけど…。手続き型の考え方だけを信奉する方たちに、脱手続き型の考え方を布教して、改宗させるための解説としてはこの記事はどうも腑に落ちない部分がありました。

なぜ繰り返すのでしょうか。0から100未満の数字がほしいだけなのに

という指摘なのですが、

Array.from({length: 100}, (v, k) => k);

という記述が、0~100の数の集合を生成する方法としては、ぼくには直感的だとは思えなかったので、

なぜ(こんな小難しい方法で)配列を生成するのでしょう。0から100未満の数字がほしいだけなのに

とゆうツッコミを入れる余地が、逆に、ありそうだなと感じました。for 文による繰り返し処理を、配列の生成にすり替えているだけに過ぎないように感じると思います。

読者によっては「なぜ繰り返すのか」にしても「なぜ配列を生成するのか」にしても、答えは「そうすると問題を解決できるから」という同じところに落ち着くだろうと感じるかもしれません。

5855contribution

@nakataSyunsuke さん、読んでいただきありがとうございます。

なぜ(こんな小難しい方法で)配列を生成するのでしょう。

そのとおりですね。これはJavaScriptに、他の多くの言語にはある、rangeがないからですね。
で、もうちょっといい書き方があったので記事をアップデートしました。(bestではありませんが、betterにはなったかと。)
ご意見ありがとうございました。

249contribution

ukiuni@github さん、ありがとうございます。

シェル芸的にパイプライン処理するような問題へのアプローチ、ぼくは大好きなんですけど、このアプローチの弱点として自分が気づいていることなのですが、パイプライン型の処理の場合、入れ子になった for 文を(トリッキーでない)自然なやり方では再現できませんねっ。

例えば、

x{10, 9.98, 9.96, , 9.98, 10}
y{10, 9.98, 9.96, , 9.98, 10}
z{10, 9.98, 9.96, , 9.98, 10}
とする時に、
x2+y2+z210 を満たす全ての点を求めて、それらの点の f(x,y,z) の和を求める。

みたいな場合ですね。問題自体の性質としてはこの記事のものと大して違いませんが、この問題では for 文をつかってそれを入れ子にするほうが簡単に思えますよねっ?

今回の記事が for 文に焦点を当てるものだということで、なおかつ、普段 JavaScript を全く使わない JavaScript の素人として、質問ですが、 JavaScript で使われているフレームワークでは、このような問題をエレガントに解決できるものですか?

9354contribution

エイプリルフールネタだとは思いますが、あえてコメントを。

「0から100未満の偶数のみを累計する」みたいなコードってそんなにありますかね。実際のループ処理の大多数を占めるであろう配列の要素に対するループだと、for ... ofとかありますし、array.forEachとかあって、ループ変数を初期化してカウントする、という作法はあまり必要なくなっています。

ジェネレータとかは一応ありますが(繰返し目的でJSで使うケースはPythonよりもかなり少ないように観測される)、素数が100個になるまで、みたいなループ数が明示的に外からは分からないものとか、中断が要件に入ると、こういうスタイルって崩れませんかね?末尾再帰とかパターンマッチがない言語なので、無理に禁止すると余計に複雑になるケースも多いかと思います。for ... of/ArrayのforEach/filter/map/reduce/every/someあたりで簡単に済む程度にとどめておいて、が無難な気がします。

async/awaitも非同期を「手続き型に記述できるようにしていく」という新文法ですし、JSのここしばらくの進化の基本的路線な気がしますし、イテレータプロトコルを自作できるようになったので、for ... ofに寄せていくというのもありな気がします。

4312contribution

@ukiuni@github rangeについては、スプレッド演算子を用いて、以下のようにも書けます。

[...Array(100).keys()].filter(v => v % 2 === 0).reduce((sum, v) => sum + v, 0)
// => 2450

https://qiita.com/akameco/items/a2b698dd4a067754997b


それと趣旨とはずれますが、今回の命題だとO(1)で計算できるので、そもそも繰り返す必要もないのかなとちょっと思いました。

const evenTotal = n => {
  const x = n % 2 === 0 ? n : n-1
  return x/2 * (x/2-1)
}

evenTotal(100)
// => 2450
evenTotal(101)
// => 2450

@nakataSyunsuke さん
できますよ、そうJavaScriptならね

func.js
const f=()=> 1;

const xArr=[...Array(10000).keys()].map(i=> -10+0.02*i).filter(a=> -10<=a && a<=10);
const yArr=[...Array(10000).keys()].map(i=> -10+0.02*i).filter(a=> -10<=a && a<=10);
const zArr=[...Array(10000).keys()].map(i=> -10+0.02*i).filter(a=> -10<=a && a<=10);

console.log(xArr
            .map((x, i)=> [ xArr[i], yArr[i], zArr[i] ])
            .filter(p=> Math.sqrt(p[0]*p[0]+p[1]*p[1]+p[2]*p[2])<=10)
            .reduce(total=> Array.isArray(total) ? 2 : total+f()));

const procType=()=>{ // 手続き型関数
    let total=0;
    for( let i=0; i<xArr.length; i++ ){
        if( Math.sqrt(xArr[i]*xArr[i]+yArr[i]*yArr[i]+zArr[i]*zArr[i])<=10 ){
            total+=f();
        }
    }
    return total;
}

console.log(procType());

JavaScriptではcallbackの第二引数が配列のkeyだと決まっているからです。第3引数はだいたい配列自身を返します。
苦しいのが.reduce(total=> Array.isArray(total) ? 2 : total+f()));の部分で、確か仕様で最初に呼ばれるのはtotalがnullでnextに第0要素が入ります。次がnull+next=nextで配列になるのでArray.isArray(total)trueになるのが2回目なので2を返しています。
JavaScriptの仕様上でArrayとnumberの足し算は文字列に変換されるためこの処理が必要です。

実行環境はnodeですが大体のブラウザで実行できると思います。

よく考えたら、reduceだけが問題なので

modFunc.js
let total=0;
for( const p of xArr.map((x, i)=> [ xArr[i], yArr[i], zArr[i] ]).filter(p=> Math.sqrt(p[0]*p[0]+p[1]*p[1]+p[2]*p[2])<=10)) total+=f(p);

console.log(total);

のほうがよっぽどスマートだった...
reduceが最強な他の関数型が使える言語とはちょっと毛色が違うのでJavaScriptで関数型を使うときは注意が必要ですね。
そしてやっぱりfor文は必要だった。

参考
MDN Array.prototype.map
MDN Array.prototype.reduce

私はシェルスクリプト派だが、言いたいことはよくわかる。シェルスクリプトの世界でも同じ例示ができる。

こんな風に書かないで
i=0
while [ $i -lt 100 ]; do
  if [ $((i%2)) -eq 0 ]; then
    totalOfEvenNumberUnder100=$((totalOfEvenNumberUnder100+i))
  fi
  i=$((i+1))
done
こう書けばよかろう
totalOfEvenNumberUnder100=$(seq 0 99 | awk '$1%2==0{print}' | awk '{i+=$1} END{print i}')

啓蒙頑張ってもらいたい。

Sign up for free and join this conversation.
Sign Up
If you already have a Qiita account log in.