- ブラウザURL入力
- DNS名前→IP
- ルータ経路選択
- ロードバランサ振り分け
- Nginx受け口
- アプリ処理今ここ
- DB保存
依頼文に書かなかったことは消えずに残り、実装した誰かが黙って決めます。今回は、書かれずに残りやすい場所として入力の端を見ます。
まず結論
依頼文が入力の端(空・巨大・範囲外)を書かなければ、その振る舞いは消えずに残ります。決めるのは、どの関数を選んだかと、どの環境で動かしたかです。
空の配列に JavaScript 標準の関数を当てると、例外・0・-Infinity・NaN・undefined・true の6通りが返り、どれも言語仕様どおりです。一方、巨大な入力の上限は仕様に無く、Node 24.21.0 の既定では `Math.max(...配列)` が約12.4万要素でスタックが溢れます。
「数値の配列を受け取り、合計・最大・平均を返す」を JavaScript 標準の関数で書きました。空の配列を渡すと、何が返るでしょうか。
当てることが目的ではありません。外した予測ほど記憶に残るので、直感で選んでください。
依頼文が書くのは真ん中だけ
「数値の配列を受け取り、合計・最大・平均を返す」。この依頼文を読んで思い浮かべるのは、[3, 8, 5] のような普通の配列です。普通の配列なら、どう書いても同じ答えになります。
書かれていないのは、入力の端です。空の配列、12万件の配列、ページ番号に -1 や 1.5 が来たとき。依頼文が黙っていても、プログラムは何かを返します。何を返すかは、書かなかった分だけ別の誰かが決めます。
決めている者は1種類ではありません。
- ・空と範囲外の答えは言語仕様に書かれている
- ・巨大の上限は仕様に無く、版・設定・呼び出し方で動く
空の配列1つに、6通りの答え
空の配列 xs = [] に、標準の関数を当てました(macOS 26.5.2 arm64 / Node v24.21.0)。
sum reduce (no init) => throws TypeError: Reduce of empty array with no initial valuesum reduce (init 0) => 0Math.max(...xs) => -Infinityavg => NaNxs[0] => undefinedevery => true
どれも不具合ではありません。6つとも ECMA-262 に書かれた振る舞いです。
It is a TypeError if the array contains no elements and initialValue is not provided.
Math.max は Let highest be -∞𝔽. から始めるので、引数が無ければ -Infinity。平均は 0/0 で NaN。every は In particular, for an empty array, it returns true. です。
合計の2行を並べると要点が見えます。違いは , 0 を付けたかどうかだけで、例外か0かが分かれます。 どちらも依頼文を満たしています。空のときを依頼文が決めていないので、実装した人か AI が、選んだと気づかないまま決めました。
空の配列に対して `xs.reduce((a,b)=>a+b)` が TypeError を投げました。これをどう捉えるのが正確ですか。
範囲外は例外にならず、それらしい値になる
空の配列には、少なくとも言語の答えがありました。範囲外の値はどうか。
「ページ番号 page(1始まり)と件数 size を受け取り、そのページの要素を返す」を1行で書きます。
const pageOf = (xs, page, size) => xs.slice((page - 1) * size, page * size);
7要素 a〜g で、依頼文が想定していない値を渡しました。
page=-1 size=2 -> ["d","e"]page=0 size=2 -> []page=1 size=-2 -> ["a","b","c","d","e"]page=1.5 size=2 -> ["b","c"]page=1 size=1000000000 -> ["a","b","c","d","e","f","g"]
例外は1つも出ません。slice は If start is negative, it is treated as length + start と定めるので、page=-1 は slice(-4, -2) として後ろから数えた d と e を返しました。page=1.5 は計算すると slice(1, 3) になり、b と c です。範囲外の値が、もっともらしいデータとして返ってきます。 空の配列より見つけにくい端です。
第16期「落ちた場所と、壊れた場所は違う」は、user.address が undefined なのは正しいのかを問い、「比べる基準が書かれていない場所では、そこが壊れているとも壊れていないとも言えません」と書きました。page=-1 で d と e が返るのが正しいかどうかも、依頼文が決めていなければ誰にも判定できません。
巨大な入力は、仕様ではなく環境が決める
空と範囲外は、言語仕様が答えを持っていました。巨大な入力は違います。
仕様の Math.max は Given zero or more arguments と書くだけで、個数に上限を置いていません。ところが Node 24.21.0 の既定では、スプレッドで渡す配列が約12.4万要素を超えると止まります。
n=124330 -> 1n=124340 -> 1n=124400 -> throws RangeError: Maximum call stack size exceededn=125000 -> throws RangeError: Maximum call stack size exceeded
決めているのは V8 のスタックです。既定の大きさは 984 KB で、--stack-size=2000 にすると約25.4万まで動きました。1プロセス内で二分探索すると 124,325、子プロセスを起動して探すと 124,321〜124,322 と、呼び方で数件〜数十件ずれます。
文字列も同じ形です。仕様の文字列型は最大 2^53−1 要素で、repeat が仕様上 RangeError を投げるのは If n < 0 or n = +∞ のときだけです。それでもこの環境では 536,870,888 文字(2^29−24)が上限で、1文字増やすと RangeError: Invalid string length でした。
仕様が決めていない端を、実装と環境が決めています。 版や設定を変えれば、同じコードの答えが変わります。
Node 24.21.0 の既定で `Math.max(...配列)` が約12.4万要素を超えると RangeError になりました。この上限を決めているのは何ですか。
空の配列
結果は ECMA-262 が関数ごとに決めており、答えが仕様に書かれています。ただし、どの関数を選ぶかで結果が変わるので、選んだ実装者が空のときの振る舞いを決めたことになります。
書き方 決めたもの 結果reduce 初期値なし 言語仕様 TypeErrorreduce 初期値 0 言語仕様 0Math.max(...xs) 言語仕様 -Infinity
巨大な配列
仕様は個数を制限していません。止まる位置は V8 の既定スタックと呼び方が決め、設定を変えると動きます。選んだ実装者だけでなく、動かした環境も決めています。
条件 決めたもの 動いた上限既定(984 KB) V8 のスタック 約12.4万--stack-size=2000 V8 のスタック 約25.4万
片方は仕様どおり、片方は環境しだい。性質が違う2つに共通していることは何でしょうか。
「依頼文を書いた人は、この結果を選んだか」を両方に問うてみてください。
この章のまとめ
- 依頼文が書くのは普通の入力だけ。入力の端(空・巨大・範囲外)は、書かなければ別の誰かが決める。
- 空の配列で、TypeError・0・-Infinity・NaN・undefined・true の6通り。全部 ECMA-262 どおりで、どれになるかは関数の選び方しだい。
- 範囲外の page や size を slice に渡すと、例外にならずもっともらしいデータが返る。
- 巨大な入力の上限は仕様に無く、実装と環境が決める。
Math.max(...配列)は Node 24.21.0 の既定で約12.4万要素、repeatはこの環境(Node 24.21.0)の V8 で 2^29−24 文字。 - 仕様が決める端と、環境が決める端を混ぜない。どちらも依頼文の外。
- 依頼文には、空のときの結果、受け付ける件数、超えたときの応答を決めて書く。実測の上限を写さない。
自分の現場で確かめる
読んだだけでは身につきません。手元の環境で実際に見てみてください。
理解度チェック
読んだ直後は「分かった」と感じやすいものです。実際に解いて確かめてください。
Node 24.21.0 で `"a".repeat(2**29)` が `RangeError: Invalid string length` になりました。この上限はどこで決まっていますか。
用語集(3語)
- 入力の端
- 本コースの語。空・巨大・範囲外など、依頼文が想定した普通の入力から外れた値。書かれないまま残りやすい。
- スタック溢れ
- 関数呼び出しに使う領域を使い切ること。V8 では `RangeError: Maximum call stack size exceeded` になる。大きさは `--stack-size` で変わる。
- スプレッド構文
- `...配列` で要素を1つずつ引数として渡す書き方。要素数だけ引数が増える。