2019-10 の TC39 meeting
まとめ
決まったこと
Web compatibility issues / Needs Consensus PRs
Decorators や Class Fields のためにオブジェクトリテラルの評価順を変更する話。モチベーションがそんなにないため取りやめになった。
Array の length の最大値が 2**32-1 のはずだが、配列リテラルの実行時の動作である ArrayAccumulation ではインデックスが ToUint32 を使って 2**32-1 以下でチェックしており、明らかに範囲外アクセスを許容しているのの修正。 コンセンサスが得られた。
Dynamic Import のときに深さ優先探索の不変性を担保するために HostImportModuleDynamically と同じ Tick 内で Evaluate が呼ばれないようにする。コンセンサスが得られた。
catch のパラメーターでデフォルト引数を受け取れない制限を取っ払う。混乱を引き起こすということで取りやめになった。
以前に引き続き Array#sort が各実装で細かに違う部分を標準化する話。コンセンサスが得られた。
Atomic.{wait, notify} について既にある実装側に合わせて仕様側でも Synchronize event を導入して仕様を強固にする。コンセンサスが得られた。
String#replaceAll の議論で第一引数の RegExp オブジェクトに global フラグが付いていない場合にエラーを投げるという話があがったが、その議論が前回 Stage 4 になった String#matchAll にも波及したらしい。Breaking Change だがコンセンサスが得られた。
Stage 4 (ES 2020)
ほとんどの実装に入り、Stage 4 になった 🎉
Stage 3
Optional Chaining と Nullish Coalescing の進捗状況の共有。実装としては JavaScriptCore と V8 そして Babel, TypeScript で動かすことが出来る状態で、Stage 4 になるには後 Test262 のみとなっている。
現状として Service Worker ではまだ ES Modules は仕様にあるものの実装されていない。また同期的に読み込む importScripts についてはライフサイクルのインストール以降のものは NetworkError が出るようになっている。
Service Worker における Top-level await について議論され、概ね以下のようになるらしい。
Top-level なスコープや ES Modules 内のモジュールにおいて Top-level await は禁止される
Dynamic Imports 内のモジュールについては Top-level await は許容される
Could be possible with HTML hooks, eg if script is still 'going' once executed?
前回の会議では RegExp#exec, String#{replace, match, matchAll} の第二引数にオプションを指定すると結果が文字列ではなくインデックスを返すような仕様だったが、どうやら単純に RegExp#exec の返り値の配列に indices プロパティを付与する仕様になったらしい。
12月の会議で Stage 4 を目指すらしい。
実装状況として V8 へ実装され、IoT 向けの Moddable XS や QuickJS, Babel にも実装されている。Test262 は実装されたので残りはある程度様子を見て問題ないことを確認することと、Editor Review を通せば Stage 4 になりそう。
個人的にモダンブラウザ以外の実装も参考にしてて驚いた。
Reflect.ownKeys が key の順番を規定しているように for-in の key の順番を規定する話。Stage 3 になった。
前回と引き続き第二引数に RegExp を突っ込んだときの動作について話された。RegExp を突っ込んだ際は global フラグが付いていない場合は例外を投げることになった。また既に Stage 4 になっている String#matchAll もこの変更に追随することになった。Stage 3 になった。
前回と引き続き AgregateError#errors を enumerable にするかどうかが話され、結果としてプロトタイプに getter として定義されるようになった。Stage 3 になった。
Private Fields において TDZ によってほとんど同じコードでもエラーが異なる場合がある。
code: (js)
// ReferenceError
class C {
}
// TypeError
class C {
}
これらを両方 TypeError にする。
Stage 2
polyfill の方 std-proposal/temporal で議論されている。今後の動きとしてはさらなるフィードバックから更新していき、npm に公式 polyfill を出してコミュニティのレビューの後で仕様を固めていくとのこと。 https://gyazo.com/8c991ac51eb3cebad8a0f1df629b9ca2
ES2018 に入っている Unicode Property Escapes では Unicode Character Property にのみ対応しており、これは Unicode で定義されている文字のリストのエイリアスとみなすことが出来る。例えば \p{ASCII_Hex_Digit} は [0-9A-Fa-f] と同じ意味になり、一つの Unicode のシンボルとマッチする。
一方で Unicode Sequence Property は複数の Unicode のシンボルとマッチするもので、主に複数のコードポイントからなる絵文字とのマッチに使われる。仮に"a", "mn" そして "xyz" にマッチするプロパティがあった場合は a|mn|xyz に展開される。こちらに対応する提案。
code: (js)
const re = /\p{RGI_Emoji_ZWJ_Sequence}/u;
re.test('👨🏾⚕️'); // '\u{1F468}\u{1F3FE}\u200D\u2695\uFE0F'
// → true
Sequence Property の場合はその性質上補集合を取ることができない。
Character Property と区別するために \q にするかどうかが議論されている。 以前の fallback オプションが入ったらしい。
Map#upsert として Stage 2 になった。
Iterator Helpers (out of topic)
特に議題に上がっていないようだが、かつてはグローバルに Iterator ネームスペースを作りその中で Iterator.syncPrototype, Iterator.asyncPrototype のようにプロトタイプのみを露出する提案だったが、他のビルトインクラスと同様に Iterator と AsyncIterator をそのまま出すようになったらしい
code: (js)
const MyIteratorPrototype = {
next() {},
throw() {},
return() {},
// but we don't properly implement %IteratorPrototype%!!!
};
// Previously...
// Object.setPrototypeOf(MyIteratorPrototype,
Object.setPrototypeOf(MyIteratorPrototype, Iterator.prototype);
これによってわざわざ Object.setPrototypeOf を使って継承しないといけなかったのが、クラス構文が使えるようになりそうでよい。
code: (js)
class MyIterator extends Iterator {
// ...
}
Stage 1
Immutable.js のネイティブ実装のような提案。新たな Value Types として Record と Tuple を入れてリテラルを用意する。このオブジェクトは SameValue によって判定する場合に中身の要素が同じであるかどうかを見る。
code: (js)
a: 1,
b: 2,
c: 3,
};
// Spread も可能
// 値を取り出すときは普通の Object と同じ
assert(record1.a === 1);
assert(record1"a" === 1); // SameValue は中身の要素が一致するかを見る
assert(record1 !== record2);
assert(record2 === #{ a: 1, c: 3, b: 5 }); code: (js)
// 値を取り出すときは普通の Array と同じ
// Tuple#with によって値を変更した新たな Tuple を作る
const tuple2 = tuple1.with(0, 2);
assert(tuple1 !== tuple2);
// Spread も可能
// Tuple#push によって値を追加した新たな Tuple を作る
const tuple4 = tuple3.push(4);
// Tuple#pop によって値を取り除いた新たな Tuple を作る
const tuple5 = tuple4.pop();
Record と Tuple の値として入っていいのはプリミティブ値と Record, Tuple のみ(これらを Value Types と呼称している)となっていて、それ以外の値を入れようとすると TypeError となる。
モチベーションとしては Immutable.js で成し遂げられなかったデバッグの容易さや Spread Syntax などイディオムの書きやすさ、宣言時に Object を経由する必要がないため無駄な処理を減らすことが出来ることなどが挙げられる。
用意するメソッドについては NS Proto Appendix に記載していて、全てのメソッドに副作用がないものとなっている。また typeof 演算子については今のところ新たに "record" を用意してはどうかという話になっているが、まだ決まっていない。 最適化として純粋関数型データ構造(Purely Functional Data Structures)の手法を使うことが出来て、例えば以下のようなことが出来る。
deep equal のチェックの速度を上げる
等しいと判定するために Hash-consing を用いることが出来る
等しくないと判定するためにツリー構造の hash を保持してそれを使うことが出来る
データ構造の操作のための最適化
既にある Record, Tuple から作るときにメモリ上で再利用することが出来る(Immutable.js と同様に)
既存の実装にある最適化を使うことが出来る
これらについては特に仕様には盛り込まれないものの、Stage 4 になる前に得られた知見をまとめることになっている。
Stage 1 になった。
Making mapping over Objects more concise (Object.map) for Stage 1 (slides) | Jonathan Keslin Array#map の Object 版。今まで同じようなことをしたい場合は Object.entries を使って Array に変換し Array#reduce か for-of を使って構成する方法があるが、どちらも最適化されているとは言えない。
code: (js)
// 列挙するのにわざわざ Array を経由する必要がある
Object.entries(obj)
// 新しく Object を作っているところがあまり最適化されない
}, {});
code: (js)
const formed = {};
// 列挙するのにわざわざ Array を経由する必要がある
// 新しく Object を作っているところがあまり最適化されない
}
これを解決するために Object.map を入れる提案。第二引数で [key, value] を渡すとそれに伴って Object が作られる。
code: (js)
Object.reduce を作ったほうが汎用性がありそうな気がするが、それだと最適化的にはあまりよくないのかな。Stage 1 になったが、Object に対してというよりかはもっと汎用的に Collection 一般に対する提案として進むことになるとのこと。
関連する話題で JSON.parse の第二引数の関数でなんとか [key, value] を渡せるようになって欲しい……。
if, while の条件文の中で変数の宣言が出来るように構文を拡張する提案。
code: (js)
let foo = new Foo();
if (let data = foo.data) {
for (let item of data) {
/* A */
}
} else {
/* B */
}
Stage 1 になった。
グローバルもしくは Standard Library に UUID v4 を入れる提案。
code: (js)
const uuid = UUID();
code: (js)
import { default as UUID } from "std:uuid";
const uuid = UUID();
Stage 1 になった。
全ての Collections に変更できない Collection を返す snapshot() と読み取り専用の readOnlyView() そしてそれらから読み取りが出来る Collections に戻す diverge() を入れる提案。
例えば Map の場合は FixedMap と ReadOnlyMap を追加し、それぞれ
code: (ts)
interface Map<K, V> {
snapshot(): FixedMap<K, V>;
readOnlyView(): ReadOnlyMap<K, V>;
diverge(): this;
}
interface FixedMap<K, V> {
snapshot(): this;
readOnlyView(): this;
diverge(): Map<K, V>;
}
interface ReadOnlyMap<K, V> {
snapshot(): FixedMap<K, V>;
readOnlyView(): this;
diverge(): Map<K, V>;
}
のように定義される。Stage 1 になった。
Promise に Promise Pipelines を導入することによってネットワークの待ち時間を減らす提案。
Promise から結果を受け取りたい場合は await を使うことになるが、結果を受け取る度に MicroTask 内に処理がやってきて少なくとも 1 Tick の時間を費やすことになる(外部のネットワークが関わる処理だと 1 RTT の時間を費やす)。もし仮にある特定の Promise の結果自身には興味はないが、その結果を使って新たに処理を行いたい場合に、await を使うのではなくて提案されている Promise Pipelines を使ってその手続きを書くと無駄に時間を費やさずに処理を進めることが出来る。
code: (js)
async function foo(x, y) {
const p1 = (await x).a();
const p2 = (await y).b();
return (await p1).c(await p2);
}
これを Evantual-Send Operations を使って
code: (js)
import { E } from 'js:eventual-send';
function foo(x, y) {
// await x の結果に興味はないが (await x).a() の Promise を作りたい
const p1 = E(x).a();
const p2 = E(y).b();
// (await x).a(), (await y).b() の結果に興味はないが (await p1).c(await p2) の Promise を作りたい
return E(p1).c(p2);
}
とすることによって高速化を図れる。
https://gyazo.com/c8052bb98489498b43ac65635633355a
具体的に Fetch API で JSON を得たいときに以下のように書くことが出来る。
code: (js)
import { E } from 'js:eventual-send';
function fetchJSON(url, options) {
// Response オブジェクトを得ることなく JSON だけ得る
return E(fetch(url, options)).json();
}
仕様としては Eventual-Send Operations と HandledPromise を導入する。
code: (ts)
interface E {
(p: Promise<T>): HandledPromise<T>;
sendOnly(p: Promise<T>): HandledPromise<void>;
}
code: (ts)
// Promise のための Proxy クラス
declare class HandledPromise<T> {
// Reflect のようにデフォルトの trap を実行するための static メソッド群
static get<T>(target: Promise<T>, key: keyof T infer K): Promise<TK>; static getSendOnly<T>(target: Promise<T>, key: keyof T): void;
static has<T>(target: Promise<T>, key: keyof T): Promise<boolean>;
static hasSendOnly<T>(target: Promise<T>, key: keyof T): void;
static set<T>(target: Promise<T>, key: keyof T, value: any): Promise<boolean>;
static setSendOnly<T>(target: Promise<T>, key: keyof T, value: any): void;
static delete<T>(target: Promise<T>, key: keyof T): Promise<boolean>;
static deleteSendOnly<T>(target: Promise<T>, key: keyof T): void;
static applyFunction<T>(target: Promise<T>, args: Parameters<T>): Promise<ReturnType<T>>;
static applyFunctionSendOnly<T>(target: Promise<T>, args: Parameters<T>): void;
static applyMethod<T>(target: Promise<T>, key: keyof T infer K, args: Parameters<TK>): Promise<ReturnType<TK>>; static applyMethodSendOnly<T>(target: Promise<T>, key: keyof T infer K, args: Parameters<TK>): void; // カスタム trap を定義する場合はコンストラクタから作る
constructor(
callback: (
resolve: (result: T) => void,
reject: (reason: any) => void,
resolveWithPresence: (handler: AsyncHandler<T>) => ({}),
) => any,
unfullfilledHandler: AsyncHandler<T>,
): this;
}
// Proxy のようにカスタム trap を定義するためのハンドラー
interface AsyncHandler<T> {
get<T>(target: Promise<T>, key: keyof T infer K): Promise<TK>; getSendOnly<T>(target: Promise<T>, key: keyof T): void;
has<T>(target: Promise<T>, key: keyof T): Promise<boolean>;
hasSendOnly<T>(target: Promise<T>, key: keyof T): void;
set<T>(target: Promise<T>, key: keyof T, value: any): Promise<boolean>;
setSendOnly<T>(target: Promise<T>, key: keyof T, value: any): void;
delete<T>(target: Promise<T>, key: keyof T): Promise<boolean>;
deleteSendOnly<T>(target: Promise<T>, key: keyof T): void;
applyFunction<T>(target: Promise<T>, args: Parameters<T>): Promise<ReturnType<T>>;
applyFunctionSendOnly<T>(target: Promise<T>, args: Parameters<T>): void;
applyMethod<T>(target: Promise<T>, key: keyof T infer K, args: Parameters<TK>): Promise<ReturnType<TK>>; applyMethodSendOnly<T>(target: Promise<T>, key: keyof T infer K, args: Parameters<TK>): void; }
基本的に Eventual-Send Operations のみで大体の要求を満たすことが出来るが、HandledPromise をコンストラクタから自前で作ることでカスタマイズすることも出来る。
Stage 1 になった。
前述した Eventual-Send Operations のシンタックスシュガーとして ~. 演算子を導入する提案。
code: (js)
import { E } from 'js:eventual-send';
function foo(x, y) {
const p1 = E(x).a();
const p2 = E(y).b();
return E(p1).c(p2);
}
を ~. を使って
code: (js)
function foo(x, y) {
const p1 = x ~. a();
const p2 = y ~. b();
return p1 ~. c(p2);
}
と記述できるようになる。Stage 1 になった。
今の ECMAScript では Out Of Memory について言及していないため、安全のために直ちにプログラムを終了する必要があることを記載する提案。Stage 1 になった。
その他
仕様文書の更新箇所が共有された。
Annex B について文法がちゃんと記載されていないため、今後新たな仕様を入れた場合にちゃんとバリデーションが出来ない問題がある。
Intl において string enum の値をどのケースにするかをどのようにして決めていったか記載してある。
総括
今回は globalThis が Stage 4 になり、Proise.any が Stage 3 になり順調に進んだ。また Record / Tuple や Promise Pipelines といった新たな提案が Stage 1 になり JavaScript の書きやすさを損ねないようにメモリや待ち時間などの最適化を図る提案が来たのが面白い。