原文はこちら。
The original article was written by Christian Wirth (Manager/Researcher at Oracle Labs).
https://medium.com/graalvm/graalvm-supports-ecmascript-2021-and-beyond-e12cdd288561
GraalVMはモダンでハイパフォーマンスなJDKディストリビューションですが、その影響は従来のJavaアプリケーションをはるかに超えています。
GraalVM
https://www.graalvm.org/
GraalVMは、Javaだけでなく、他のJVM言語(ScalaやKotlinなど)で書かれたアプリケーションのパフォーマンスを向上させます。そのNative Imageジェネレータは、Javaアプリケーションを事前コンパイルし、優れた起動性能と低いメモリ要件を持つネイティブ実行ファイルに変換します。GraalVMは、JavaScript、Python、Ruby、Webassembly、Rなど、他の多くのプログラミング言語もサポートしています。
Graal.jsと呼ばれることの多いJavaScriptの実装は、実はGraalVMの現時点での最も完成度の高い実装です。
GraalVM JavaScript Implementation
https://www.graalvm.org/javascript/
この実装では、スタンドアロンのJavaScriptアプリケーションの実行や、Javaアプリケーション内でのJavaScriptの実行をサポートしています。また、Node.jsを完全にサポートしており、注目すべきは、JavaScriptの言語仕様の最新版であるECMAScript Language Specification, version 2021と互換性があることです。
この記事では、ECMAScript 2021によってJavaScriptに追加された機能を見て、GraalVMを使うことでどのような恩恵を受けることができるのか、その例を紹介します。また、読むより動画という方向けに、同じテーマを扱っている動画もあります。この動画では、Christian WirthがECMAScript 2021の新機能を説明し、GraalVMを使った例を紹介しています。
ECMAScript 2021
JavaScriptは、ECMAScript言語仕様で規定されています。JavaScriptはECMAのTechnical Committee 39 (TC39)によってオープンソースで開発されており、多くのJavaScriptのベンダー、ユーザー、コントリビューターがメンバーとして参加しています。彼らは一緒になって、JavaScriptがポジティブでメンテナンス可能、かつ生産的な方法で進化することを確認しています。この委員会では、既存のコードを壊さないというweb compatibility(Web互換性)の維持が大きな関心事となっています。
Oracle Labsは、昨年からTC39に参加し、積極的に活動しています。委員会のメンバーになったことで、GraalVMはJavaScriptの将来の開発について直接的なインサイトを得ることができ、仕様策定の過程で興味深い提案を積極的に行うことができるようになりました。
2021年3月には、ECMAScript 2021になるような草案がTC39で合意されました。ECMAの総会ではまだ承認されていませんが、これは形式的なものだと感じています。すべての関係者はすでに、このドラフトが既存のコードを壊さないことを確認しており、総会では、よく練られ、よく定義された仕様を承認するでしょう。
ECMAScript 2021では、いくつかの機能が公式に言語に追加されます。ほとんどの実装では、それらの機能は、デフォルトまたはフラグをつけることで、すでに利用可能です。GraalVMでは、GraalVM 21.0.0以後でこうしたECMAScript 2021の新機能はデフォルトで利用できます。それ以前のリリースでは、明示的に以下のフラグをつけることでこれらの機能のサブセットを入手できます。
js.ecmascript-version=2021
String.prototype.replaceAll
まずは簡単な例から。この新しいStringメソッドは、検索パターンのすべての出現箇所をreplaceの値で置き換えます。
> "the quick brown fox".replaceAll("o","_")
the quick br_wn f_x
既存の String.prototype.replace
メソッドと同様に、検索値は文字列または正規表現を、置換値は定数文字列または検索されたパターンを入力とするreplacer関数を利用できます。これまでは、replaceをStringパターンで使用する際には、繰り返しreplaceを呼び出す必要がありましたし、グローバルな正規表現パターンを使用することもできました。
Graal.jsではこの機能をサポートしており、何の制約もありません。
Private Methods
プライベート・メソッドは、クラスでの既存の可能性を拡張するものです。メソッドの名前の前に#記号を付けることで、メソッドをプライベートに宣言できるようになりました。
class Example {
fnPublic() {
console.log("fnPublic called");
}
#fnPrivate() {
console.log("fnPrivate called");
}
}
const ex = new Example();
ex.fnPublic();
ex.#fnPrivate(); //fails with an error!
クラス内からメソッドを呼び出す際には、実際に#記号が必要になることに注意してください。このようなメソッドは、クラス内からは呼び出し可能ですが(例えば、fnPublic()
から呼び出し可能)、クラス外からは呼び出しできません。これは、Java のメソッドで private キーワードを使用するのと似ており、他の言語でも同じような機能があります。
Private Accessors
Privateクラスアクセッサメソッドは上述のプライベートメソッドに類似しています。
class Example {
get name() {
return "ECMAScript";
}
get #version() {
return 2021;
}
}
const ex = new Example();
console.log(ex.name);
console.log(ex.#version); //error!
getterの名前の前に #記号を付けると、getterがプライベートになります。このgetterは、外部からではなく、クラス内部から呼び出し可能です。これにより、オブジェクト内のデータのカプセル化が簡単になり、より良いAPIを書くことができるようになるでしょう。
Promise.any and AggregateError
このpromiseコンビネータは、入力されたいくつかのpromiseを集約し、 そのうちのどれかが解決した場合にはその集約されたpromiseを解決します。promiseのイテレート可能な形式で指定すると、 ひとつのpromiseを返します。この1個のpromiseは、解決済みのpromiseのうちのひとつの値で解決します。入力promiseが解決されない場合、結合されたpromiseは新しい種類のエラー、AggregateError
で拒否されます。
const pr1 = new Promise(resolve => { resolve("pr1 worked"); });
const pr2 = new Promise(resolve => { resolve("pr2 worked"); });
//const pr2 = Promise.reject("pr2 failed");
Promise.any([pr1, pr2])
.then(result => console.log("result: "+result))
.catch(err => console.log("error: "+err+" "+err.errors));
Promise.anyはPromise.allと似ていますが、入力のうち少なくとも1つが解決した場合に解決します。
すべての入力promiseが拒否された場合、catch
節でAggregateErrorを受け取ることになります。これは、個々のpromiseのすべての拒否メッセージを組み合わせたものです。これらのメッセージは、errors
プロパティで照会できます。
Promise.any([pr1, pr2])
.then(result => console.log("result: "+result))
.catch(err => console.log("error: "+err+" "+err.errors));
このようにAggregateError
を使用することで、promiseが失敗した理由を理解することができ、それに応じた対応が可能になります。
WeakRef and FinalizationRegistry
弱参照では、ガベージコレクションが必要となる可能性のあるオブジェクトへの参照を格納できます。JavaScript は自動化されたメモリ管理システムを持っています。アプリケーションからアクセス可能なすべてのオブジェクト(および配列、関数など)は維持されますが、スコープがすでに閉じられているためにアクセスできなくなったオブジェクトはメモリから削除されます。
このため、不要になったオブジェクトを保持したままだと、いわゆるメモリリークが発生することがあります。典型的な例は、オブジェクトをキーにしたキャッシュです。この状況では、(キャッシュの外では)オブジェクトが実際にはもう使われていないかもしれないのに、キャッシュの中ではオブジェクトの強参照が保持されがちです。キャッシュはメモリリークの原因となります。キャッシュは、とっくに消えているはずのオブジェクトを「生きたまま」にします。キャッシュから再びオブジェクトを削除する戦略がなければ、プロセスがメモリ不足になるまで、アプリケーションのメモリ消費量は増え続けます。
その問題を解決できるのが弱参照です。弱参照は、オブジェクトへの参照を保持していますが、強参照がなくなった場合、メモリ管理システムがオブジェクトを削除できます。つまり、すべての強参照がなくなり、オブジェクトへの1つ以上の弱参照だけが存在するようになると、ガベージコレクタはそのオブジェクトを削除できます。その後、弱参照からnull
値を受け取ることになります。キャッシュの例では、null
になった弱参照をキャッシュから削除する必要がありますが、これは簡単な操作で、メモリリークは解決します。
let obj = {id: “The object to remember”, counter: 0};
let weakRef = new WeakRef(obj);
let strongReference = weakRef.deref();
if (strongReference != null) {
//use strongReference here
}
FinalizationRegistry
FinalizationRegistry
を使うと、弱参照が再取得されたときに特定のコールバックが呼び出されるように指定できます。
const registry = new FinalizationRegistry(heldValue => {
console.log(“registry called on: “+heldValue);
});
registry.register(obj, “held value”);
上記の例ではobjを登録しています。ガベージコレクションされると、コールバックが呼び出され、引数としてhold valueを渡すので、オブジェクトを特定できます。
Logical Assignment Operators
新しい論理代入演算子として、論理OR代入演算子 ||=
、論理AND代入演算子 &&=
、論理NULL代入演算子 ??=
の3つが追加されています。これらの演算子は、チェックと短絡的な動作の保持の両方を実現するために、これまでifを必要としていたコードを簡素化します。次のようなケースを考えてみましょう。
val = (val || default);
このコードでは、変数valに有効なエントリがあるかどうかを確認しています。valが使用可能かどうか(truthyかどうか)をチェックし、使用できない場合はdefaultを割り当てています。しかし、これは完全に正しいとは言えません。このコードが効果的に行っているのは、どのような場合でも値を割り当てることです。つまり、valの以前の値が(再)割り当てられるか、デフォルト値が割り当てられるかです。これは大きな違いではありませんし、実際には多くのJavaScriptエンジンがこの違いを無視して最適化することができるので、大きな違いではありませんが、違いが生じるケースもあります。例えば、valがアクセサプロパティの場合、2回評価されます。まず、getterが実行され、値が取得され、その値がtruthyかどうかがチェックされます。たとえそれがtruthyであったとしても、setterを使って同じ値を割り当てますが、getterやsetterに観測可能な副作用がない限り、実際の効果はありません。ですから、実際にやりたいことは、次のような、より不器用な変形かもしれません。
if (!val) {
val = default;
}
こちらだと、効果は同じでもやや可読性に欠ける表現です。
val || (val = default);
論理代入演算子は、チェックと代入を1つの操作にまとめることで、その操作を単純化し、より読みやすくしました。上の行の代わりに、次のように書けばよいことになります。
val ||= default;
これはvalがtruthyであるかどうかをチェックし、falsyの場合のみdefaultを代入します。valがすでにtruthyである場合、それ以上は何も起こりません(つまりvalのgetterは一度だけ実行されます)。
同様に,論理AND代入演算子は,値がすでにtrueである場合にのみ代入し,論理null代入演算子は,値がnullish(nullまたはundefined)である場合にのみ代入します。
val &&= other; //equivalent to previous: val && (val = other);
val ??= other; //equivalent to previous: val ?? (val = other);
Numeric Separators
最後に、非常に軽いけれども有用な話題です。コード中の数値定数の区切り文字としてアンダースコア文字_が使えるようになりました。概念的には、数字の意味に影響を与えない千単位の区切り文字と考えてください。これは読みやすさを向上させるだけです。なお、数値を千の単位で区切ることに限定されているわけではないので、どのような区切り方も可能です。
const million = 1_000_000;
const double = 1_234_567_890.123;
const fraction = 0.000_001;
const binary = 0b1010_0001_1000_0101;
const hex = 0xA0_B0_C0;
数字を解析する際、アンダースコアが取り除かれ、残りの数字が期待通りに解析されます。数字をアンダースコアで始めることはできませんし(識別子として解析されてしまいます)、数字をアンダースコアで終わらせることもできません。また、アンダースコアは連続して1つしか使用できません。これは小数点にも当てはまり、アンダースコアで小数点を挟んではいけません。言い換えれば、アンダースコアを小数点の区切りとして使用する場合は、厳密に両側に数字が必要です。
Looking beyond ECMAScript 2021
Graal.jsは、抽象構文木(AST)インタープリタとして実装されています。Polyglot APIは、この作業を容易にし、優れたパフォーマンスを実現するのに役立っています。ASTのノードに様々な特殊化を施すことで、インタープリタは実行中に投機的に自分自身を最適化することができ、結果としてパフォーマンスを最適化することができます。GraalVM自体は、他のJavaコードと同様にASTインタープリタをコンパイルし、ASTから高度に最適化されたマシンコードを生成します。突き詰めるところ、これにより、Graal.jsチームが通常は相反する2つの目標をたった1つの実装で達成できるようになりました。
- 1つの実装をASTノードとして提供するだけなので、機能の実装とメンテナンスが容易になった
- (しかも)実行時にマシンコードにJITコンパイルされているため、結果として得られるエンジンの優れた実行性能を実現した
GraalVMでは、ECMAScriptの新機能を採用する際には、仕様書と全く同じ手順で、抽象的な仕様書を具体的なASTノードで表現するだけでよく、それだけで完全かつ高性能な実装が可能になります。
このシンプルさのおかげで、私たちは今後予定されているいくつかの機能(TC39が言うところのプロポーザル)を検討し始めました。
ECMAScript proposals
https://github.com/tc39/proposals
これらのプロポーザルは、ステージ0からステージ4(完成)まで、5つのステージに沿った5段階のプロセスを踏んでいます。ECMAScript 2021に搭載された前述の機能は、すべてステージ4にあります。以下では、ステージ2または1のプロポーザルについて説明します。いくつかのプロポーザルは、進化サイクルのかなり初期の段階にあり、採用されるまでにセマンティクスとシンタックスのより大きな変更を受けるものもあります(もし委員会に受け入れられることがあればの話ですが)。
Operator Overloading
Operator overloading in JavaScript
https://github.com/tc39/proposal-operator-overloading
演算子のオーバーロードは、C++などでおなじみの機能ですが、JavaScriptでは、クラスのみに演算子のオーバーロードを提供する安全でクリーンな方法が提案されています(現在はまだステージ1です)。演算子オーバーロードは、ほとんどの場合syntactic sugar(構文上のおまけ)ですが、言語レベルでの健全性を維持しつつ、多くのプログラムを簡素化し、可読性を向上させることができます。
GraalVMでは、相互運用性のユースケースを考えると、演算子オーバーロードは特に有望です。GraalVMは、異なるプログラミング言語間でコードやデータを共有する(例:JavaのMapやPythonのArrayにJavaScriptでアクセス)のに最適です。演算子をオーバーロードすることで、必要な変換を指定したり、他の問題に取り組むための良い方法になるでしょう。もちろん、他の言語を持ち込むと、「オーバーロードを許可している他の言語とどのようにやりとりするか」「このコンテキストでオーバーロードをどのように指定するか」などの問題が出てきて、この取り組みは非常に複雑になります。
GraalVMでは、現在のバージョンの仕様のうち、オーバーロードに関する部分の完全な実装ができています。しかし、現在の提案ではまだ詳細に答えられていない未解決の問題がいくつかあります。例えば、既存のコードとの衝突を避けるために、演算子のオーバーロードは、関連するコードブロックに対して明示的に有効にする必要があるでしょう。そのための解決策の一つとして、新たにwith operators from
句を提案しています。Graal.jsではまだこれをサポートしておらず、現時点ではグローバルにオーバーロードを有効化(または無効化)しています。
Decorators
Decorators
https://github.com/tc39/proposal-decorators
デコレーターは、Javaのアノテーションに似ています。クラス、フィールド、メソッドにアノテーションを宣言的に付与する方法を提供します。
残念ながら、現在の提案の状態では、ステージ4に到達して承認されるまでに、まだいくつかの変更が予想されます。現在の状態を実装したブランチをオープンにしていますが、いくつかの変更がまだ仕様書に反映されていないため、現在はマージできません。仕様書の作成者が今後の方針について合意に達した時点で、私たちは実装を更新し、かなり早い段階でサポートを提供できるようになります。
この提案は、Oracle Labsが協力しているJohannes Kepler University Linz (JKU Linz) の学士課程の学生(Florian Huemer)によって実装されました。 お疲れ様でした。
Temporal
Temporal
https://github.com/tc39/proposal-temporal
Temporalは、新しい日付・時刻ライブラリです。JavaScriptの既存のDate
オブジェクトが抱えている問題を解決し、TimeZonesや時間、日付、期間などのあらゆる種類の抽象化をサポートする大規模な新しいAPIをもたらします。この提案は、最近、ステージ3に達したため、良好なテストカバレッジ、複数のエンジンでの実装、完全で洗練された仕様書など、すべての要件が満たされれば、仕様書に採用される見込みです。
GraalVMは現在、完全な仕様書の実装に取り組んでいます。(PlainTime
、PlainDate
、PlainDateTime
、PlainYearMonth
、PlainMonthDay
に加え、Calendar
とDuration
をサポートする)不完全なプレビュー版は、GraalVM 21.2リリース(js.temporal
フラグが必要です)で利用可能になる予定で、21.3では完全なサポートが期待されています。
Decoratorsの提案と同様に、この機能は Johannes Kepler University Linz (JKU Linz) の学士課程の学生による実装に基づいています。実装してくれたKortiに大きな拍手を送ります!
Conclusion
Graal.jsはECMAScript言語仕様の完全で互換性のある実装で、ECMAScript2021仕様の最新機能をサポートしています。新機能は定期的に追加されますので、最新版のGraalVMを利用すると、最新のJavaScriptの今後の機能の多くを試すこともできます。GraalVMはオープンソースですので、もしあなたが貢献に興味があれば、ECMAScriptのプロポーザルや他の仕様変更、その他の改善のためのプルリクエストを喜んで受け付けています。あなたが取り組んでいることを私たちに教えてください。
GraalJS
https://github.com/oracle/graaljs