なぜ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文を書いたほうが、短くプログラミングできることもありますからね。。。。失敬)
現場からは以上です。
ぼくも関数型の考え方が大好きで多用しているので全体の趣旨としては賛成なんですけど…。手続き型の考え方だけを信奉する方たちに、脱手続き型の考え方を布教して、改宗させるための解説としてはこの記事はどうも腑に落ちない部分がありました。
という指摘なのですが、
という記述が、0~100の数の集合を生成する方法としては、ぼくには直感的だとは思えなかったので、
とゆうツッコミを入れる余地が、逆に、ありそうだなと感じました。
for
文による繰り返し処理を、配列の生成にすり替えているだけに過ぎないように感じると思います。読者によっては「なぜ繰り返すのか」にしても「なぜ配列を生成するのか」にしても、答えは「そうすると問題を解決できるから」という同じところに落ち着くだろうと感じるかもしれません。
@nakataSyunsuke さん、読んでいただきありがとうございます。
そのとおりですね。これはJavaScriptに、他の多くの言語にはある、rangeがないからですね。
で、もうちょっといい書き方があったので記事をアップデートしました。(bestではありませんが、betterにはなったかと。)
ご意見ありがとうございました。
ukiuni@github さん、ありがとうございます。
シェル芸的にパイプライン処理するような問題へのアプローチ、ぼくは大好きなんですけど、このアプローチの弱点として自分が気づいていることなのですが、パイプライン型の処理の場合、入れ子になった
for
文を(トリッキーでない)自然なやり方では再現できませんねっ。例えば、
みたいな場合ですね。問題自体の性質としてはこの記事のものと大して違いませんが、この問題では
for
文をつかってそれを入れ子にするほうが簡単に思えますよねっ?今回の記事が
for
文に焦点を当てるものだということで、なおかつ、普段 JavaScript を全く使わない JavaScript の素人として、質問ですが、 JavaScript で使われているフレームワークでは、このような問題をエレガントに解決できるものですか?エイプリルフールネタだとは思いますが、あえてコメントを。
「0から100未満の偶数のみを累計する」みたいなコードってそんなにありますかね。実際のループ処理の大多数を占めるであろう配列の要素に対するループだと、for ... ofとかありますし、array.forEachとかあって、ループ変数を初期化してカウントする、という作法はあまり必要なくなっています。
ジェネレータとかは一応ありますが(繰返し目的でJSで使うケースはPythonよりもかなり少ないように観測される)、素数が100個になるまで、みたいなループ数が明示的に外からは分からないものとか、中断が要件に入ると、こういうスタイルって崩れませんかね?末尾再帰とかパターンマッチがない言語なので、無理に禁止すると余計に複雑になるケースも多いかと思います。for ... of/ArrayのforEach/filter/map/reduce/every/someあたりで簡単に済む程度にとどめておいて、が無難な気がします。
async/awaitも非同期を「手続き型に記述できるようにしていく」という新文法ですし、JSのここしばらくの進化の基本的路線な気がしますし、イテレータプロトコルを自作できるようになったので、for ... ofに寄せていくというのもありな気がします。
@ukiuni@github
range
については、スプレッド演算子を用いて、以下のようにも書けます。https://qiita.com/akameco/items/a2b698dd4a067754997b
それと趣旨とはずれますが、今回の命題だと
O(1)
で計算できるので、そもそも繰り返す必要もないのかなとちょっと思いました。@nakataSyunsuke さん
できますよ、そうJavaScriptならね
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だけが問題なので
のほうがよっぽどスマートだった...
reduceが最強な他の関数型が使える言語とはちょっと毛色が違うのでJavaScriptで関数型を使うときは注意が必要ですね。
そしてやっぱり
for
文は必要だった。参考
MDN Array.prototype.map
MDN Array.prototype.reduce
私はシェルスクリプト派だが、言いたいことはよくわかる。シェルスクリプトの世界でも同じ例示ができる。
啓蒙頑張ってもらいたい。