Dev Study
← 何を作るかを言葉にする
レッスン 6 / 10·約 8 分·第21期 何を作るかを言葉にする

入力の端を書かなければ、実装と環境が決める

リクエストがアプリケーションに届くまで
  1. ブラウザURL入力
  2. DNS名前→IP
  3. ルータ経路選択
  4. ロードバランサ振り分け
  5. Nginx受け口
  6. アプリ処理今ここ
  7. 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)。

$ node l6-empty.mjs(該当6行を抜粋。他の行は省略)コマンド出力
sum reduce (no init) => throws TypeError: Reduce of empty array with no initial value
sum reduce (init 0) => 0
Math.max(...xs) => -Infinity
avg => NaN
xs[0] => undefined
every => 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 で、依頼文が想定していない値を渡しました。

$ node l6-page.mjs(範囲外の5行を抜粋)コマンド出力
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万要素を超えると止まります。

$ node l6-maxone.mjs <n>(1プロセス1回、トップレベルで実行。8行中4行を抜粋)コマンド出力
n=124330 -> 1
n=124340 -> 1
n=124400 -> throws RangeError: Maximum call stack size exceeded
n=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 になりました。この上限を決めているのは何ですか。

⇄ 2つを見比べる
具体例 A

空の配列

結果は ECMA-262 が関数ごとに決めており、答えが仕様に書かれています。ただし、どの関数を選ぶかで結果が変わるので、選んだ実装者が空のときの振る舞いを決めたことになります。

空の配列で決まったこと表
書き方 決めたもの 結果
reduce 初期値なし 言語仕様 TypeError
reduce 初期値 0 言語仕様 0
Math.max(...xs) 言語仕様 -Infinity
具体例 B

巨大な配列

仕様は個数を制限していません。止まる位置は V8 の既定スタックと呼び方が決め、設定を変えると動きます。選んだ実装者だけでなく、動かした環境も決めています。

Math.max(...配列) が動いた要素数(Node 24.21.0)表
条件 決めたもの 動いた上限
既定(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 文字。
  • 仕様が決める端と、環境が決める端を混ぜない。どちらも依頼文の外。
  • 依頼文には、空のときの結果、受け付ける件数、超えたときの応答を決めて書く。実測の上限を写さない。

自分の現場で確かめる

読んだだけでは身につきません。手元の環境で実際に見てみてください。

環境:

理解度チェック

読んだ直後は「分かった」と感じやすいものです。実際に解いて確かめてください。

問題 1 / 3正解: 0

Node 24.21.0 で `"a".repeat(2**29)` が `RangeError: Invalid string length` になりました。この上限はどこで決まっていますか。

用語集(3語)
入力の端
本コースの語。空・巨大・範囲外など、依頼文が想定した普通の入力から外れた値。書かれないまま残りやすい。
スタック溢れ
関数呼び出しに使う領域を使い切ること。V8 では `RangeError: Maximum call stack size exceeded` になる。大きさは `--stack-size` で変わる。
スプレッド構文
`...配列` で要素を1つずつ引数として渡す書き方。要素数だけ引数が増える。