範囲指定のインターフェースについて
あるとき、チーム内で「範囲指定の設計」について話題になりました。
具体的には、開始と終了の値をどちらも含む形にするか、終了を含まない形にするかという点です。
開始、終了の引数をそれぞれbegin,endとすると、
どちらも含む設計は
私はこれまで「endは含まない」という設計が自然だと考えてきました。
例えば、配列のインデックスが0から始まり、.slice()の終了位置が含まれないことに慣れていたからです。
ですが、改めて「なぜそうなっているのか?」を掘り下げてみると、意外と根拠がはっきりしないこともありました。
この記事では、この「終了を含む or 含まない」問題について、自分なりに調べて考えたことを整理してみます。
エドガー先生の考察
こちらのQuoraの回答が参考になりました。
ほとんど全てが人間が後から作ったものであるコンピューター上のこと関して言えば、「直感的」と言う言葉にはそれほど意味はなく、どれが一番慣れているか、という答えに帰着する場合が多いです。また、0か1かだけではなく、終わりが含まれるか含まれないか、ということも関係してきます。
この回答でEdsger W. Dijkstraという大学教授のエッセイが紹介されています。
2, 3, ... , 11, 12 という整数の範囲を表現するのに、4つの式表現が考えられます。
a.
b.
c.
d.
どの表現がよいのか、それぞれのメリットや考慮できることをまとめてくれています。
まずa, bは、以下の理由から扱いやすいと語られています。
- 境界値の差が部分列の長さに等しい
- 隣接する範囲を表現するとき、一方の上限がもう一方の下限に等しい
さらに下限に指定した自然数を含まない b は、最初の自然数よりも小さな値 (the realm of the unnatural numbers) を含むのは気持ち悪い (ugly) として、a の表現が良いとしています。
また、Mesaというプログラミング言語では、特別な表記法により4つすべてが記述できる言語らしいです。しかしaのパターン以外の3つはいつもバグの温床になってしまうことがわかり、使用しないことが強く推奨されるようになったということです。
まとめ
エドガー先生の考察は私にとって非常に納得できるものですが、プログラミングとして絶対的な正解はないということも理解しました。そのため、基本的にはプログラミング言語ごとの言語仕様や文化に倣うべきだろうと考えています。
JavaScriptでは、インデックスを0で始め、範囲指定では「beginを含み、endは含まない」設計が広く使われています。
- String.prototype.substring()
- Array.prototype.slice()
その一方でDOM APIでは「end を含む」設計も存在します。
※こちらのコメントにて教えて頂きました。
私のプロジェクトは Full-Stack TypeScript で構成されており、「end は含まない」方針を基本として採用していく予定ですが、一口に範囲指定といっても様々なケースがあるので、その文脈をきちんと考慮してチームで混乱が発生しないよう設計していこうと思います。
Discussion
DOM の Range だと 始点と終点を指定しますね。(含まない表現のしようがないはありますが。)
DOM の TimeRanges だと 始点と終点を指定しますね。
HTMLInputElement の selectionStart / selectionEnd だと 始点 と終点 を指定しますね。
EditContext の selectionStart / selectionEnd だと 始点 と終点 を指定しますね。
DOM 周りだと 始点 終点 の index の指定パターンになるみたいですね。(css も index ではなく番号ですがその傾向があります。
たくさんの事例を紹介していただいてありがとうございます。
確かに「DOM 周りだと 始点 終点 の index の指定パターンになる」というケースが多そうですね!
私が書いた「JavaScriptでは、 ... 概ね統一されています。」の表現は適切ではなさそうなので、後ほど修正します。少々お待ち下さい m(_ _)m
「end を含む」設計も存在することを本文に記述させていただきました。