2023 年 11 月 28 日追記:第 99 回 TC39 ミーティングが行われたことに伴い記事を更新しました。また、サンプルコードにある致命的なミスを発覚し修正しました。
はじめて技術記事を書いてみました。ぐらふぃーむと申します。
ECMAScript(いわゆる JavaScript)の先端を操る TC39 に関する情報が(少なくとも日本語コミュニティでは)思うより少なかったため初回は「SmooshGate 事件」と「Array Grouping プロポーザル」を取り上げようと思います。
拙い文章なのでおかしいところがあればご指摘願います。編集リクエスト機能もご活用ください。
SmooshGate 事件
2023 年になって Array.prototype.flat
メソッドを知らない JavaScript デベロッパーはほとんどいないでしょう。しかしその裏にはあまり知られていない、メソッドの名前や運命に関わる出来事があります。それが「SmooshGate 事件」です。
メソッド自体を解説する文章ならいくらでもありますのでそれを省きさせていただきます。念のため MDN と仕様書へのリンクです。同様に TC39 の紹介も省きます。
Array.prototype.flatten
これが元々のメソッド名です。「flatten」の方が動詞ですし Lodash の方にちなんだ名前を取って 2017 年 7 月1、Array.prototype.{flatten,flatMap}
というプロポーザルが立てられました。それからはずっと順調で、半年も掛からず Stage 3 に達しました。
しかし、翌年となる 2018 年 3 月、問題が発覚されました。
これがブラウザで実装されるとサイトが壊れる!
MooTools というビルトインオブジェクトの prototype をいわゆる monkey-patch するライブラリを使っているサイトがこのプロポーザルによって壊されるとのこと。昔はかなり有名なライブラリで、多くのサイトに使われているため、無視することができません。
なぜ問題は MooTools によって引き起こしたのか
説明はやや厄介になるので飛ばして構いません。
MooTools では仕様に従わない独自の flatten
メソッドが定義され、Array.prototype
に直接代入します。よって Array.prototype.flatten
がブラウザで実装されるとしても、MooTools のコードは後で実行するので当然ネイティブの実装を上書きします。特に何もないように見えますが——
あいにく、MooTools のやることはこれだけではありません。MooTools には Elements
という独自の API があり、Array.prototype
に代入された独自メソッドは以下のように Elements.prototype
にコピーされます:
for (var key in Array.prototype) {
Elements.prototype[key] = Array.prototype[key];
}
ここで注意したいところは、for-in
ループは列挙可能(enumerable)なプロパティしか反復しないことです。仕様書の第 18 章の部分によりネイティブメソッドはすべて列挙不可能になっており、古いバージョンにはない Object.defineProperty
などを使わない限り値が書き換えられてもプロパティの属性は変えることがありません。よって Array.prototype.flatten
がネイティブに実装されたらメソッドは Elements.prototype
にコピーされませんので、Elements.prototype.flatten
に依存するコードがあるすべてのサイトが壊れることになります。
そこで「Array.prototype.flatten
を特別に列挙可能にすればいいじゃん」と思うあなたは大間違いです。ES5 以前は for-of
さえもないので、Array.prototype.forEach
を使わざるを得ません。しかし関数型プログラミングを嫌がっている人やパフォーマンス面に配慮のある人はみんな for-in
ループを使って配列の各要素を反復していました。もしも Array.prototype.flatten
を列挙可能にすると、"flatten"
プロパティは "0"
、"1"
、"2"
……などのように for-in
ループに含まれることになるので、さらに多くのコードが壊されるでしょう。
どうせ古いサイトばっかだし、そのまま破ったら?
海外では有名な Space Jam など、古いサイトでも閲覧できるままであり続けるよう、破壊的変更(breaking change)を極力せず Web 全体を守るという HTML、CSS、ECMAScript などの Web に関わるすべての標準にも適用される原則があります。それが「Don’t break the Web」です。
仮に何かの新しい機能が搭載されることによってサイトが動かなくなったら、突如閲覧できなくなる訪問者だけでなく、何もしてないのに直さなければならないサイトの所有者もデベロッパーも困ります。さらに「別のブラウザでは動くじゃん」と気づいたユーザーはそのまま使っているブラウザから去るため、市場シェアを失ったブラウザベンダーは困り、他のブラウザは実装を拒否するので、標準化担当の方々も困ります。すべての関係者にも不利益をもたらす最悪の結末になりかねます。
改名へ
話は戻ります。みんなが名前を何にしようと議論を始めた際、あるプルリクエストが出されました。この冗談半分に出したメソッド名を flatten
から smoosh
に変えるプルリクは広汎な議論を呼び、‑gate という「不祥事」を意味する英語の接尾辞にちなんで誰かが事件を「SmooshGate」とまでハッシュタグ付きで名付けました。
そして 2018 年 5 月、TC39 の第 64 回会議にて flat
に改名することが決まり2、事件は一件落着しました。
2019 年 1 月、Array.prototype.{flat,flatMap}
プロポーザルは Stage 4 に達し、正式に仕様書に載せることを果たしました。
一難去ってまた一難——Array Grouping プロポーザル
一難ところか二難です。Array Grouping という 2021 年 7 月に立てられたプロポーザルもまた前述のプロポーザルと同じくらいな速さで、同年 12 月に Stage 3 に達しました。
Stage 3 に達した時点で、提案は Array.prototype.groupBy
と Array.prototype.groupByToMap
3 の形を取っていました:
["hoge", "fuga", "foo", "piyo", "bar", "baz"].groupBy(({ length }) => length);
// { 4: ["hoge", "fuga", "piyo"], 3: ["foo", "bar", "baz"] }
["hoge", "fuga", "foo", "piyo", "bar", "baz"].groupByToMap(({ length }) => length);
// Map { 4: ["hoge", "fuga", "piyo"], 3: ["foo", "bar", "baz"] }
しかし、翌月となる 2022 年 1 月、問題が発覚されました。
サイトがまた壊れる!
今度はブラウザが Array.prototype.groupBy
を搭載すると Sugar.js という別の monkey-patching ライブラリの特定のバージョンを使う多くのサイトが動かなくなります。
Sugar.js はネイティブ実装の有無によってメソッドを置き換えるかどうかを決めます。Sugar.js の実装は "hoge.fuga[0..3].piyo[-1]"
のような文字列による再帰的プロパティアクセスや第2引数などの独自機能があり、Array.prototype.groupBy
をブラウザ側が実装すると古いバージョンの Sugar.js は上書きしないため、これらの独自機能が使えなくなります。これによって、メソッド名の変更を余儀なくされました。
そして、同年 6 月の第 90 回会議にて group
への改名が決まりました。それからの 11 月——
Web 互換性問題、再び
group
という単語はごく一般的なため、User[] & { group?: "admin" | "member" | undefined }
のような形で、普通のプロパティ名として使われがちです。この例では、ユーザーグループは設定済みかどうかを真偽チェックするなどが考えられます。実際にこれに似た原因で URL が ……/login/realms?group=function group() { [native code] }
になってしまうケースも見られます。
また、発覚されたこれらの互換性問題により Google Chrome はバージョン 108 で Array.prototype.{group,groupToMap}
を実装することを諦めました。
そして現在に至り、新しい名前として groupToObject
や grouping
や grouped
や groupedBy
などの候補はありますが、轍を何度も踏まないよう、新たな選択肢は作られました。
2022 年 11 月の第 93 回 TC39 ミーティングにて静的メソッド Object.groupBy
と Map.groupBy
にする提案がプルリクとともに出されました。これは将来的に WeakMap.groupBy
や Records & Tuples プロポーザルにおける Record.groupBy
に拡張することを踏まえると、こちらの方がやや好まれるそうです。
Object.groupBy(["hoge", "fuga", "foo", "piyo", "bar", "baz"], ({ length }) => length);
// { 4: ["hoge", "fuga", "piyo"], 3: ["foo", "bar", "baz"] }
Map.groupBy(["hoge", "fuga", "foo", "piyo", "bar", "baz"], ({ length }) => length);
// Map { 4: ["hoge", "fuga", "piyo"], 3: ["foo", "bar", "baz"] }
翌年 11 月、提案はこの形で Stage 4 に達し、仕様の一部になりました。
おわりに
Array.prototype.{flat,flatMap}
と Array Grouping という二つの ECMAScript プロポーザルの歴史を紹介しました。TC39 の方々は Web を壊さないよう多大な努力をしていることが分かります。
この初めての記事はすごく時間が掛かりました。それでも技術的な話だけでなく文章を書くコツについても沢山学びましたので、それはそれで値すると思います。皆さんのご意見をコメント欄でお待ちしております。
謝辞
こちらの記事を参考にさせていただいてはじめて、この文章を書き上げることができました。ありがとうございます。
- Don’t break the Web:以 SmooshGate 以及 keygen 為例(繁体字中国語)(前半)
- SmooshGate FAQ(英語)(前半)
- Stage 3 Array Grouing プロポーザル の Web の互換性の問題 by @sosukesuzuki(後半)