Edited at

JavaScriptで数値の区切り文字を使いたい物語

桁数の多い数値をプログラム中に書く場合は、1_234_567のように間に区切りを入れることで桁数を分かりやすくしたいことがありますよね。実際、既に多くのプログラミング言語でこれが可能です。Java, Kotlin, Swift, Perl, Ruby, Rustなどの言語で1_234_567が可能です。また、C++は1'234'567のように_ではなく'を用いて区切るのが特徴的です。

JavaScriptにおいてはまだこれは不可能ですが、できるようにしようという提案は古くから知られていました。それがnumeric separatorというプロポーザル(提案)です。日本語に直すと「数値区切り文字」とかでしょうが、何となくしっくり来ないのでこの記事ではnumeric separatorと呼ぶことにします。当該のプロポーザルは2017年4月に起草され、同5月にはStage 1に認定されています。そして、何やかんやがあって2019年3月にStage 3に昇格しました。

そんなに実装が難しいものでもありませんし、今年中にブラウザ等の実装が出揃ってES2020に正式採用という流れかなあと見ています。また、今すぐ使いたい場合はBabelのプラグインを使いましょう。またTypeScriptもすでに対応済みです。


numeric separatorの使用法

では、もう少し詳しくnumeric separatorのルールを見ていきましょう。区切り文字は10進数だけでなく全ての数値リテラルで使用可能です。_は何個でも使用できます。

1_234_567     // 10進リテラル

1_234.567_89 // 小数も可能
0xdead_beef // 16進リテラル
0o7_5_5 // 8進リテラル
0b0101_0111 // 2進リテラル

ただし、_が使用可能なのは数字と数字の間です。端に使うことはできません。また、_を複数連続させることもできません。

_123_456 // これは_が左端にあるので不可

123_456_ // これは_が右端にあるので不可
123__456 // これは_が2個連続しているので不可
123_.456 // . の隣も端と見なされるので不可

また、コーナーケースとしては、JavaScriptでは実は2.3e5のような数値リテラルも可能です(これは230000になります)が、eの直前直後も端となるのでだめです。

2.3e1_0 // これは可能

2.3_e10 // これは不可

話はこれだけです。とても簡単ですね。


文字列から数値への変換に対する影響

ところで、JavaScriptでは文字列から数値への変換を行う言語機能があります。大きく分けて、parseIntparseFloatによるものとNumberによるものです。文字列から数値への暗黙の変換は後者に含まれます。今回数値リテラルで_が許されるようになったということで、これらにはどのように影響するのでしょうか。

結論から言うと、影響しません。言うまでもなく、これは後方互換性を維持するための措置ですね。

この結果として、parseIntNumber、そして生のソーステキストの3つが文字列に対してそれぞれ異なる解釈を持つこととなります。

まず、parseIntは10進リテラル(1e5のような指数部分を含むものを除く)と16進リテラルを解釈可能ですが、8進と2進は解釈できません。これは後ろ2つがES2015で追加されたものであり、そこで後方互換性を壊さない判断をされたことが理由です。

// 10進と16進は対応

console.log(parseInt("123")); // 123
console.log(parseInt("0x123")); // 291
// 8進と2進は未対応
console.log(parseInt("0o755")); // 0
console.log(parseInt("0b101")); // 0

一方、Numberによる変換は8進・2進リテラルにも対応しています。

console.log(Number("123"));   // 123

console.log(Number("0x123")); // 291
console.log(Number("0o755")); // 403
console.log(Number("0b101")); // 5

では、今回_が追加されてどうなったかといえば、parseIntNumberも未対応となります。

console.log(parseInt("1_234_567")); // 1

console.log(Number("1_234_567")); // NaN

歴史が感じられるなかなか素晴らしい仕様ですね。


numeric separatorの歴史

さて、やっていることはわりかし簡単なこの提案ですが、実はこの提案が現在の状態になるまでには紆余曲折がありました。この記事ではその部分にスポットを当てて解説しようと思います。


起草〜Stage 3への出世

最初に述べた通り、このプロポーザルは2017年4月にStage 1となりました。StageというのはJavaScriptへの機能追加の提案が通る承認フローであり、Stage 0からStage 4までの段階があります。仕様が具体化されているかどうかなど、ステージを上がるごとに必要な条件は厳しくなっていきます。Stage4になればその提案は完成と見なされ、JavaScriptに正式採用となります。Stage 1というのはアイデアはまあ良いんじゃないと認められた状態です。Stage 2は具体的な仕様をちゃんと作ろうという状態、Stage 3は仕様がほぼ完成して実際に試してみたいという状態です。この承認フローを司るのがTC39という委員会です。

区切り文字の提案を作るにあたって区切り文字の有力な候補は_ですが、C++の'のように他の候補も一度ありました。また、_が複数連続するのが許されるかどうかなどの細かい論点もありました。しかし、Stage 1に上がったときの議事録Stage 2のときの議事録を見るに、この辺は特に異論なくすんなりと進んだようです。

最後のNumberの挙動についてはStage 3に上がるときの議論ではNumber("1_234_567")1234567になるという話だったのですが、その後ひっそりと現在の仕様に変更されたようです

このように、numeric seprator自体の議論は特に大きな波乱もなく順調に策定が進み、2017年11月にはStage 3に上がりました。これは提案が起草されてからわずか半年というスピード感ある出世です。Stage 3というのは仕様がほぼ完成している段階であり、ブラウザやnode.jsなどの実行環境が実装を始める段階です。また、TypeScriptもStage 3まで上がった仕様は取り入れるという方針を取っており、2018年2月に登場したTypeScript 2.7で利用可能になりました。

この流れならES2018とかES2019に正式採用されてもおかしくはなかったのですが、そうはなりませんでした。ライバルが出現したのです。


ライバルの出現

颯爽と現れたnumeric separatorのライバル、それはextended numeric literalsです。この提案は、2017年9月にStage 1に入りました。numeric separatorがStage 3に上がるすこし前ですね。

これはやはり数値リテラルを拡張する提案で、およそ以下のような提案でした。数値のあとにこのように_pxなどの接尾辞を付けます。

const length = 10_px;

const imaginary = 13_i;

ポイントは、この_px_iなどを自分で定義できる点です。関数として_pxなどを予め定義しておくことで、10_pxのようなリテラルを自分で定めることができるのです。

const _px = ({ number })=> ({ type: "px", length: number });

const length = 10_px;

console.log(length); // { type: "px", length: 10 }

初期には_pxではなくpxのように任意の変数名を接尾辞に使用可能な提案でしたが、さすがに無理があるということで最初に_を付けることになりました。

ちなみに、この_pxなどは10進だけでなく16進・2進・8進などの数値リテラルにも付けることができます。

さて、ここでひとつの問題が発生しました。それは文法に曖昧性が発生してしまうというものです。具体例としては0x123_abcが挙げられます。そう、これは0x123abcの間に区切り文字が入ったという解釈と、0x123_abcという接尾辞がついたという解釈の2通りが可能なのです、

この問題が発生してしまった以上、2つの提案はどちらかが変更される必要があります。この問題をめぐって2つの提案は膠着状態に入りました。


numeric separatorの降格

この問題が2018年5月のTC39ミーティングで取り上げられた際、numeric separatorはStage 2に降格となってしまいました。プロポーザルのステージが下がるのはなかなかレアな現象です。

解決策として_px__pxにする(アンダースコア2個)や`pxにする、_pxは10進数のリテラルのみに許すなどの案もありましたが、どの案も合意に達することはなく、2つの提案は協調しながら仕様を再設計することとなりました。

そして、Stage 3だったnumeric separatorも仕様が変動する可能性があるということでstage 2に戻されました。

ところで、numeric separatorは一度Stage 3に上がったのでTypeScriptに取り入れられていました。これがStage 2に戻ってしまったということで、これはTypeScript側に打撃を与えました。TypeScriptユーザーが既にこれを使用しているかもしれず、この機能がStage 2に戻ったからといって削除してしまうのは破壊的変更となります。また、そもそもnumeric separatorの仕様自体が変更となってしまう可能性もあり、その場合TypeScriptは非標準のものを実装していることとなってしまいます。直す場合はやはり破壊的変更です。

では、TypeScript側はどうしたのでしょうか。答えは、TypeScriptチーム主要メンバーのRyanCavanaughさんがちゃぶ台を投げました

Screenshot from Gyazo

そして、その後は見て見ぬふりを貫きました。まあ、実際に事が起こってから(どうしようもない仕様変更が起こってから)動こうという考えだったのでしょう。


解決編

しばらく2つの提案が水面下で動いたあと、2019年1月のミーティングで動きがありました

概要は、extended numeric literalsを変更して100~pxのようにするというものです。すなわち、チルダを使って_pxではなく~pxとするようになりました。100~pxというリテラルを解釈するときにはnumeric__pxという関数が呼ばれます。

この新提案がTC39のミーティングにかけられましたが、あまり受けは良くありませんでした。特に、~pxに対してnumeric__pxという関数を作らなければいけないのが微妙です。

このような仕様となっている理由は例えば~iとしたい場合にiという関数を用意しないといけないのはそれはそれで微妙(ループ変数にiという変数名を迂闊に使えなくなるので)だからでしたが、やはりnumeric__は受け入れがたかったようです(議事録を見ると賛否両論があったようですが)。

こうなってくると、もはや他の変数たちと同じ名前空間でやっていくのは無理がある気がします。再び無理難題をつきつけられてしまったextended numeric literalsでした。

しかし、ここである存在が救いの手を差し伸べました。それにより、extended numeric literalsは2019年3月のTC39ミーティングに早くも返り咲くことができたのです。このミーティングでは、extended numeric literalsはみたび新たな姿を見せました。

今回の新提案では、~px@pxとなりました。記号が変わっただけじゃんと思うかもしれませんが、熱心なJavaScriptユーザーはあることにお気づきでしょう。そう、@というのはデコレータの記号です。デコレータは現在Stage 2の提案ですが、@を用いて関数やクラスのメンバーを修飾できるという提案でした。実は、123@pxと書いた場合は@pxというデコレータが参照されることになります。スライドから例を引用しましょう。

decorator @i { @numericTemplate(impl) }

1234@i

このように、@iという接尾辞を使うためには事前にデコレータ宣言により@iというデコレータを宣言しておきます。その実態は@numericTemplateという組み込みデコレータです。こう書くと、1234@iimpl(Object.freeze({string: "1234", number: 1234}))という意味になります。

ポイントは、デコレータは変数名とは別の名前空間を持つため@iというデコレータを作ってもそんなに問題とならないだろうという点です。デコレータは変数に比べてそれほど頻繁には作られないため、プログラマが十分制御できると考えられます。

グローバルデコレータ名前空間にデコレータを増やして解決するという方向性は、少し怖く見えるかもしれません(標準ライブラリ導入の動機と一見逆行しています)。しかし、デコレータ宣言もスコープを持つため同名の組み込みデコレータが増えてもエラーが発生しないこと、デコレータの存在確認をランタイムで行う方法は(恐らく)提供されないことからこれは問題にはならなそうです。また、将来的には組み込みデコレータも標準ライブラリから提供できるようになるでしょう。

まあ、extended numeric literalsに関してはまだStage 1なので、基本はこの方向性でいきつつまだまだ大きな変化があると考えられます。とはいえ、これによりわざわざ_を使う理由は無くなりました。

そろそろ話をnumeric separatorに戻しましょう。上述の変更により2つの提案の衝突は解消されました。よって、2019年3月のミーティングでnumeric separatorはそのままの形で晴れてStage 3への復帰を果たしたのです。

さすがにもう邪魔する者はないと信じたいですね。そのうちブラウザ等にも実装されるでしょう。


まとめ

数値リテラルの数字の間に_を挟めるという単純な話でしたが、その裏にはこのようなドラマがありました。numeric separatorsを救ってくれたデコレータへの感謝の気持ちを込めて_を数字の間に挟みましょう。


関連リンク