2020-06 の TC39 meeting
まとめ
決まったこと
Web compatibility issues / Needs Consensus PRs
Promise.{all, allSettled, race} の現在の仕様ではまず第一引数の Iterable から Iterator を取得し、その後で this.resolve が函数かどうかのチェックをしているが、もしチェックに失敗した場合に Iterator を開放する処理が必要になってしまっている。
code: (js)
function all(iterable) {
...
var iterator = iterable.#SymbolIterator();
try {
var promiseResolve = this.resolve;
if (typeof promiseResolve !== "function")
} catch (error) {
try {
iterator.return();
} catch {
}
throw error;
}
// Since for-of loop once more looks up the @@iterator property of a given iterable,
// it could be observable if the user defines a getter for @@iterator.
// To avoid this situation, we define a wrapper object that @@iterator just returns a given iterator.
var wrapper = {};
wrapper.@@iterator = function() { return iterator; };
for (var value of wrapper) {
...
Iterator を取得する前に this.resolve をチェックしたほうがシンプルになるのでそうしようという提案。承認された。
Stage 4 (ES2021)
code: (js)
"foobarfoo".replaceAll("foo", "baz"); // "bazbarbaz"
文字列を引数にマッチする箇所を全て置換するメソッドの提案。Stage 4 になった 🎉
Stage 3
変数に代入されたときに函数の name プロパティをどうするかが議論されている。
もともと Function#name は無名関数でも代入演算子を使って定義された場合にその変数名になるようになっている。
code: (js)
const foo = () => {};
console.log(foo.name); // "foo"
これを Logical Assignment でも合わせようという話。
code: (js)
let foo;
foo ??= () => {};
console.log(foo.name); // "foo"
実装が困難でないという条件付きでコンセンサスが得られた。
AggregateError クラスの constructor の細かな仕様について話されている。
Stage 2
Decorators に求められる"静的"とはパース時もしくは初回コンパイル時のコストのみで実行される必要がある。そしてそれは3つの戦略に分けられる。
1. Definition Static
Built-in Decorators の提案がこれに当たる。
code: (js)
export decorator @tracked {
@initialize((instance, name, value) => {
instance[__internal_${name}] = value;
})
@register((target, name) => {
Object.defineProperty(targer, name, {
get() { return this[__internal_${name}] },
set() { this[__internal_${name}] = value, this.render(); },
configurable: true,
});
})
}
宣言時に静的解析ができ、いくつかのオペレーターに分解できるようになっている。また実行前にパースされ、影響範囲を把握することができる。異なるデコレーターで基本的な動作が変わるので、異なる方法で最適化することになる。
2. Application Static
Read/Write Trapping Decorators の提案がこれに当たる。
code: (js)
function logged(enabled) {
return () => {
return {
get(target, instance, prop, value) {
return value;
},
set(target, instance, prop, value) {
if (enabled) {
console.log(prop, value);
}
return value;
},
};
};
}
Decorators は1つだけの意味を持ち、前もって変換する事ができる。動作が1つだけなためデコレーターごとに同じ最適化を施すことになる。
3. Built-Target Static
ビルドツールによって Decorators を無糖化された形式に変換できる。脱糖された後の動作は動的かもしれないが、あくまで変換は静的に行うことができる。
これらのすべての戦略を満たす方法があるかどうかはまだわかっていない。
また Decorators のユースケースの調査が進んでいる。
書記素や単語、文で文字列を分割できる提案。
Intl.Segmenter#segment が返す Iterable な %Segments% から、もとの Intl.Segmenter のインスタンスが取れるようにするかどうかが議論されている。
仕様のテキストを作れれば先に進む状態。仕様の作り方として3つのテクニックがあり、それぞれトレードオフがある。
1. A naive generetors
単に Iterator の prototype に Generator Functions なメソッドを登録していく方法。
code: (js)
Iterator.prototype.map = funciton* map(f) {
for (let value of this) {
yield f(value);
}
};
実装が簡単な反面、エラーを即座に投げられなかったり、Iterator#{return, throw} に対応できない。
code: (js)
let source = new UserDefinedIterator();
let mapped = source.map(x => x.name);
// does not call source.return()
// because the generator hasn’t entered the for loop yet.
mapped.return();
2. New iterator "classes"
Iterator#map を呼び出すと IteratorMap クラスを返すようにする。メソッドごとにクラスが必要になるため仕様がとてつもなく長くなってしまうし、非同期の場合はより酷いことになる。
3. Built-in generators
既存の Gererator Functions を使うのではなく、Iterator Helpers のメソッドそれぞれに Built-in な仕様を与える。今まで Built-in な Generator Functions がなかったため仕様に新たに Yield のステップを追加する必要がある。
https://gyazo.com/aab12de93576586b394af55f3ac94c46
これについては issue で議論される。
Cookbook が完成したとのこと。DOM API 側の仕様にも触れられている。 code: (js)
// <input type="date">
const datePicker = document.getElementById('calendar-input');
const today = Temporal.now.date();
datePicker.max = today;
datePicker.value = today;
前回との変更点としては Calendar と Custom Time Zone が挙げられている。
code: (js)
// Calendar
// adds the number of days in that month of the Hebrew calendar
date.withCalendar('hebrew').plus({ months: 1 });
// Custom Time Zone
class MyTimeZone extends Temporal.TimeZone {
constructor() { super('Some_Identifier'); }
getOffsetNanosecondsFor(absolute) { ... }
getPossibleAbsolutesFor(dateTime) { ... }
*getTransitions(absolute) { ... }
}
今後のロードマップとしては Stage 3 にするために issues を解決し、polyfill を使っているユーザーからのフィードバックをもとに polyfill を更新するとのこと。公式 polyfill が TypeScript サポートしていて堅牢になっている。 調査をしているようなのでぜひ回答を。
函数の実装の中身を隠す提案。
Stage 3 にはならず、"sensitive" については実装者からの懸念が呈された。
2つの数値から文字列を作る Intl.NumberFormat#formatRange や useGrouping オプションの追加、string として与えたときに number にキャストせずに Decimals として扱うなどの更新がある。
Stage 2 になった。
期間の文字列を生成する提案。Temporal.Duration#toLocaleString に対応付けされる。
code: (js)
const formatter = new Intl.DurationFormat("en-US", {
fields: [
"hour",
"minute",
"second",
],
style: "short",
hideZeroValues: "none",
});
const duration = Temporal.Duration.from({
hours: 2,
minutes: 46,
seconds: 40,
});
// "2 hr 46 min 40 sec"
console.log(formatter.format(duration));
Stage 2 になった。
Realms, Stage 2 updates (slides) | Caridy Patiño, Leo Balter API をシンプル化した。
code: (ts)
declare class Realm {
constructor();
readonly globalThis: typeof globalThis;
import(specifier: string): Promise<Namespace>;
}
Realm#evaluate がなくなったおかげで既存の CSP の unsafe-eval や default-src を再利用できるようになったとのこと。
Compartment も新しくなり、新たな Realm コンストラクタを持つようになった。
code: (js)
const compartment = new Compartment(options);
const VirtualizedRealm = compartment.globalThis.Realm;
const realm = new VirtualizedRealm();
const { doSomething } = await realm.import('./file.js');
Realm の使い方として例えば Third Party スクリプトとしてプラグインを接続するには
code: (js)
import { api } from 'plugingFramework';
const realm = new Realm();
realm.globalThis.api = api;
await realm.import('./plugin1.js');
のように使うことになる。
DOM API 側に Realm を入れる作業が進行中で、それによって Stage 3 がブロックされている状態。
プライベートプロパティを持っているかどうかのチェックができる機能の追加。Stage 2 になった。
import 文に attributes を追加する提案。明示的にファイルを JSON として読み込むことができる。
code: (js)
import data from "./data.json" with type: "json";
仕様には新たに MuduleRequest records を追加することになるらしい。Import Attributes として Stage 2 になった。
Stage 1
長いこと放置されていた Do expressions の提案。モチベーションが再確認された。
code: (js)
const x = do {
const tmp = random();
tmp * tmp;
};
Stage 2 にはならなかったが、Draft の仕様を書くのと Pattern Matching との兼ね合いを議論することになった。
symbol をキーとして許容するかどうかや Destructuring Syntax について話された。
code: (js)
// 今までと同じように代入演算子の左辺を Object のような記法した場合
const { foo } = #{ foo: "foo" }; asserts(foo === "foo");
const { foo, ...rest } = #{ foo: "foo", bar: "bar" }; typeof rest; // "object" or "record"?
code: (js)
// 新たに Record/Tuple 用のシンタックスを追加した場合
const #{ foo, ...rest } = #{ foo: "foo", bar: "bar" }; assert(rest === #{ bar: "bar" }); const #{ foo, ...rest } = { foo: 123, bar: {} }; // 右辺が Object のときに TypeError になりえる Records/Tuples に函数やオブジェクトを追加できるようにする提案があがっている。
言語仕様側で対応しない場合は例えば number とオブジェクトを対応付けするクラスを用意する必要がある。
code: (js)
class RefBookkeeper {
ref(obj) {
const idx = this.#references.length;
this.#referencesidx = obj; return idx;
}
deref(sym) {
return this.#referencessym; }
}
// Usage
const server = (() => {
const references = new RefBookkeeper();
port: 8080,
handler: references.ref(function handler(req) { /* ... */ }),
};
return {
structure,
references,
};
})();
server.references.deref(server.structure.handler)({ /* ... */ });
これだとメモリ解放の管理が困難になるので言語側で対応したい。そのやり方として挙げられているのに以下のものがある。
1. 新たなプリミティブとして Box を用意し、オブジェクトを保持できるようにする
code: (js)
const obj = { hello: "world" };
const box = Box(obj);
assert(typeof box === "box", "boxes are a new primitive type");
assert(obj !== box, "boxes are not their boxed object");
assert(obj === box.deref(), "boxes can deref the full object");
port: 8080,
handler: Box(function handler(req) { /* ... */ }),
};
server.handler.deref()({ /* ... */ });
問題として複数の Realms で渡しあうことができない。これについては Box インスタンス経由することで明示的に Realms で渡し合えないような仕様にして解決することはできる。
code: (js)
const obj = { hello: "world" };
const Box = new BoxMaker();
const box = Box(obj);
assert(obj === Box.deref(box), "boxes can deref the full object");
一方で Box インスタンスを管理する必要が出てくる。
2. WeakMap のキーとして Symbol を許容することで自動で GC 時にオブジェクトも回収されるようにする
code: (js)
class RefCollection {
ref(obj) {
// (Actually cache here)
const sym = Symbol();
this.#references.set(sym, obj);
return sym;
}
deref(sym) { return this.#references.get(sym); }
}
この方法が良いだろうということで Stage 1 にすべく提案された。
無事 Stage 1 になった。
Immer のように Record/Tuple で部分的に変更する場合に新たな記法を用意する提案。
code: (js)
// Immer
let complex2 = Immer.produce(complex, (draft) => {
draft.foo.arr0.counter = 1; });
// Proposal (Record/Tuple)
...complex,
};
Stage 1 になった。
Classic Scripts のために同期的な読み込みのサポートや、polyfill 可能にしないといけない問題が話された。
ビルトインメソッドを継承したクラスを制限する提案。
Array や Promise といったビルトインメソッドの継承は仕様が複雑になっており、メンテナンスコストも膨れ上がっている。ここでいっそのことその複雑さの根源である @@species を消してしまうのはどうかという提案。
もちろん Web 互換性の問題が出てくるが大体 @@species を使っているものは core-js くらい(1000 サンプルのうち該当するサイトは26件でその全てが core-js のコードだった)で、それは影響が無いものだった。
Stage 1 になった。
Intl のオプションとしてサポートしている値のリストを取得できる API の提案。
例えばサポートしているカレンダーのリストを取得するには以下のように記述すれば良い。これは“Calendar algorithm key” in Common Locale Data Repository をもとにしている。
code: (js)
Intl.getSupportedCalendars();
// ['buddhist', 'chinese', 'coptic', 'dangi', 'ethioaa',
// 'ethiopic', 'gregory', 'hebrew', 'indian',
// ...
// 'persian', 'roc', 'islamicc'];
他にも ISO 4217 をもとにした通貨の一覧や、Numbering Systems、タイムゾーン、そして単位が取得できる。
Stage 1 になった。
2つの値を比較するメソッドを用意する提案。Array#equals や Array#compare そして Speceship Operators について話された。
どうやら Array#equals が Stage 1 になったっぽい。
Array#{slice, splice} がインデックスとして負数を与えると後ろからの相対的なインデックスとして扱ってくれる。それと同様に負数も考慮して値を取得できる Array#item の提案。
Array だけではなく DOM API にある ArrayLike なクラス(NodeList など)では既に item メソッドを持っているためそれらの仕様も合わせてしまおうという目論見がある。
互換性の問題としては item が普遍的な名前なので MooTools, Ext, PrototypeJS などを使った古い JS のコードでは問題が出る可能性がある。また DOM API の方の仕様では負数で値を取得できないため(null を返す)これも問題を起こすかもしれない。
Stage 1 になった。
locales によって扱う単位が異なる場合がある。例えば距離の単位として en-US では miles が使われ、fr-FR では kilometers が使われる。
Intl.NumberFormat で単位を扱うことができるが、直接単位系を指定していたため、locales によって出し分けすることができない。そこで新たなオプションとして "usage" を追加し、適切な単位に変換してくれるようにする提案。
"usage" オプションが付いている場合は既存の "unit" オプションは入力された値の単位として扱われ、それをもとに別の単位に変換してくれる。
code: (js)
const inputValue = 1.8;
const inputUnit = "meter";
console.log(new Intl.NumberFormat('en-US', {
style: 'unit',
unit: inputUnit,
usage: 'person-height', // NEW parameter
unitDisplay: 'long'
}).format(inputValue));
// expected output: "5 feet and 11 inches"
// (1.8 meters = 5 feet 10.8661 inches")
Stage 1 になった。
Stage 0
ES Modules の namespace として予約語を用いることができない。これは CommonJS との互換性を保つことができず、WebAssembly のような予約語が共有されない環境で問題が起きる可能性がある。
そこで文字列リテラルを使って任意の namespace を使えるようにする提案。
code: (js)
export { _ as "hello world" };
import { 'nice to meet you' as _ } from 'foo';
Node.js の async_hook や Angular で使われている zone.js のように非同期なタスク内でストアを共有できる新たなクラスを作る提案。
モチベーションとしては Async Functions 内のスタックトレースが分断されてしまう問題の解決や await し忘れていてリークしてしまっているタスクを検知できるようになる。
提案されるクラスとしては AsyncLocalStorage, AsyncTask そして AsyncHook がある。
AsyncLocalStorage は enterWith メソッドで同期的にストアを作り、その Async Context 内で getStore メソッドを実行するとさっき作ったストアを取得できる。
code: tracker.js
const store = new AsyncLocalStorage();
export function start() {
// (a)
store.enterWith({ startTime: Date.now() });
}
export function end() {
// (b)
const dur = Date.now() - store.getStore().startTime;
console.log('onload duration:', dur);
store.exit();
}
code: (js)
import * as tracker from 'tracker.js';
window.onload = e => {
// (1)
tracker.start();
// (2)
return processBody(res.body).then(data => {
// (3)
const dialog = html`<dialog>Here's some cool data: ${data}
<button>OK, cool</button></dialog>`;
dialog.show();
tracker.end();
});
});
};
AsyncTask は非同期なリソースをマニュアルに宣言できる。
code: (js)
class DatabaseConnection {
constructor(port, host) {
// Initialize connection, possibly in root context.
this.socket = connect(port, host);
}
async query(search) {
const query = new Query(search);
const result = await this.socket.send(query);
// This context is triggered by DatabaseConnection
// which is not linked to initiator of DatabaseConnection.query.
return query.runInAsyncScope(() => {
// Promise linked to the initiator of DatabaseConnection.query.
// Promise -> Query(AsyncTask) -> DatabaseConnection.query
return Promise.resolve(result);
})
}
}
class Query extends AsyncTask {
constructor(search) {
// scheduled async task
super('database-query');
this.search = search;
}
}
AsyncHook は Async Task をスケジューリング、実行できる。
code: (js)
const als = new AsyncLocalStorage();
const backlog = [];
const hook = new AsyncHook({
scheduledAsyncTask (task) {
const test = als.getStore();
if (test == null) {
return;
}
backlog.push(new WeakRef(task));
}
});
hook.enable();
als.enterWith(name);
try {
await callback();
} finally {
hook.disable();
als.exit();
}
assert(
backlog.filter(ref => ref.deref() != null).length === 0,
'${name}' ended with dangling async tasks.
);
Stage 1 にはならなかった。
import 文の import と from の順番を入れ替えできるようにする提案。
その他
ECMAScript には Unicode の最新仕様を追随するように書いてあり、Unicode の更新のたびにわざわざ ECMAScript 側の仕様に手を加えないですむ……かと思いきや RegExp の Unicode Property 周りで手を加えないといけなくなっている。
総括
String#replaceAll が無事 Stage 4 になり、Iterator Helpers や Temporal, Realm, Record and Tuple, そして Intl 周りに進捗が見られた。Intl.NumberFormat の usage オプションはやりすぎだと思うが。
例によって Decorators は再出発の様相を呈していて、今後どうなるのか気になる。