sosukesuzuki.dev

2022年 sosukesuzuki 1人アドベントカレンダー

December, 10 2022

今のうちに Module Expressions と Module Declarations について整理しておこう

2022年の sosukesuzuki 1人アドベントカレンダー 10 日目です。

筆者は次の 2 つの ECMAScript のプロポーザルに注目しています。

この 2 のプロポーザルは構文のみに注目するとよく似ています。しかしそのモチベーションは全く異なります。

このプロポーザルが登場したのは 2 年ほど前ですが、現在でも仕様の策定が進んでいます。これらのプロポーザルが順調に進捗し ECMAScript に取り込まれることに備えて、それらのモチベーションを今のうちに整理しておきましょう。

まず Module Expressions も Module Declarations も、モジュールの中で新しいモジュールを作成する構文を導入するプロポーザルです(名前から想像できるとおり)。

// Module Expressions の例
const mod1 = module {
  export const exported = "exported from mod1";
};
const { exported } = await import(mod1);

console.log(exported); // "exported from mod1"
// Module Declarations の例
module mod2 {
  export const exported = "exported from mod2";
}
import { exported } from "mod2";

console.log(exported); // "exported from mod2"

このように、この 2 つのプロポーザルの構文上の違いは、単にそれが expression (式) であるか declaration (宣言) であるかということのみです。

では、それぞれのプロポーザルはどのようなモチベーションで提案されたのでしょうか。

Module Expressions

ブラウザでは Service Workers や Web Workers や Worklet などによって JavaScript からマルチスレッドを使うことができます。しかし JavaScript でのマルチスレッドプログラミングでは、いくつかの問題に直面することになります。

SharedArrayBuffer を除きメモリの共有ができないので、スレッド間で関数などのコードの共有ができません。そのため「ある関数を別のスレッドで実行する」というような典型的なパターンのために、JavaScript では関数の文字列化のような方法に頼っています。これではクロージャが機能しませんし、importfetch などのパスを解決する処理が期待どおりに動かなくなることもあります。

また、別のスレッドで動かすプログラムを別のファイルに置かなければならないというのは開発者の体験を損なわせます。

const worker = new Worker("./worker.js");

このようなコードをモジュールバンドラーでバンドルする場合、どのように設定するのがよいでしょうか。また、このようなコードをライブラリとして npm で配布する場合は ./worker.js はどこに配置すればよいでしょうか。

このように、現在の JavaScript のマルチスレッドを使うための機能は微妙に使いにくいのです。

これを解決するのが Module Expressions のモチベーションです。

Module Expressions は、モジュールの中にモジュールを作成できるようにして、さらに HTML 側で標準化されている具体的なマルチスレッドのための API (Web Workers や Service Workes など)も合わせて修正することで、JavaScript でのマルチスレッドプログラミングを簡単にしようとしています。

たとえば Module Expressions と、それに合わせて修正された Worker コンストラクタを使うと次のようになります(Web Workers 側の修正は https://github.com/whatwg/html/issues/6911 で議論されています)。

const workerMod = module {
  onmessage = async ({ data }) => {
    // data として受け取ったモジュールを import する
    const { greet } = await import(data);
    // import したモジュールが export している greet 関数を呼び出す
    const message = greet("Sosuke");
    // greet 関数の結果をメインスレッドに戻す
    postMessage(message);
  };
};

// 最初に空の Worker を作成し、後から Module Expression で作ったモジュールを追加する
const worker = new Worker({ type: "module" });
worker.addModule(workerMod);

// Worker から来た値をそのまま alert に流す
worker.onMessage = ({ data }) => {
  alert(data);
};
// モジュールに包んで関数を Worker に渡す
worker.postMessage(module {
  export function greet(name) {
    return `Hello, ${name}`;
  }
});

このコードはメインスレッドから渡された greet 関数を Worker 内で実行し、その結果をメインスレッドに戻し、それを alert で表示します。

まとめると、Module Expressions のモチベーションは JavaScript でのマルチスレッドプログラミングに関する次の問題を解決することです。

  • メインスレッドからの関数の共有ができない
  • 別スレッドで実行する処理を別のファイルに記述しなければならない

Module Declarations

(このセクションの大部分は 9 日目のアドベントカレンダーからの引用です)

Module Declaration は主に webpack などのモジュールバンドラーの出力として使われることを想定しています。

Module Declaration は、現在のモジュールバンドラーが抱えるいくつかの問題を解決します。

1 つめの課題は、モジュールバンドラーの実装が複雑になりすぎているということです。

普段多くの Web 開発者が ECMAScript Modules を使ってコードを書いています。そしてそれをモジュールバンドラーを使ってバンドルしています。このモジュールのバンドルという処理は、ネイティブの ECMAScript Modules の振る舞いをエミュレートすることと等しいです。ECMAScript Modules を使ったコードを、ECMAScript Modules を使わない形に変換するわけですから。

そのせいでモジュールバンドラーの実装は複雑になっています。モジュールバンドラーの出力を Module Declarations で表現できればその複雑さをいくらか軽減できます。

2 つめの課題は、モジュールバンドラを使うと ECMAScript Modules のコードがブラウザで実行されるときまで残らないため、JavaScript エンジンによる最適化が効かないということです。

筆者は JavaScript エンジンの実装に詳しくないので ECMAScript Modules を直接実行するときと、それに相当するバンドルされたコードを実行するときを比較して、どの程度パフォーマンスに有意差が出るかは知りません。が、一般的なプログラミング言語処理系に思いを馳せれば、ランタイムにコードが引き渡されるときに、静的に判断できる情報が多ければ多いほど最適化は効きそうな気がします。

これらを解決するのが Module Declarations のモチベーションです。

Module Declarations があることを想定して、モジュールバンドラーの挙動を考えてみましょう。

次の2つの JavaScript ファイル ./src/greeting.js./src/index.js があります。

// ./src/greeting.js
export default function (name) {
  return `Hello, ${name}!`;
}
// ./src/index.je
import { greet } from "./greet.js";

console.log(greet("Sosuke"));

これらのファイルを Module Declarations を使ってバンドルすると次のようになります。

// ./dist/index.js
module greeting {
  export default function (name) {
    return `Hello, ${name}!`;
  }
}
import { greet } from "./greet";
console.log(greet("Sosuke"));

この ./dist/index.js を見ると、./src/greeing.js の中身がそのまま greeting モジュールの中身になっています。この架空のモジュールバンドラーは export default を解釈する必要すらなくシンプルな挙動になっています。

まとめ

似た構文を持つ 2 つのプロポーザルが全く異なるモチベーションから提案されていることを述べました。

筆者としてはどちらのプロポーザルも未来の JavaScript にとって重要だと考えているので今後の動向が楽しみです。

おまけ

もともとの名前

この 2 つのプロポーザルはもともとは別の名前として提案されていました。

  • Module Expressions ... Module Blocks
  • Module Declarations ... Module Fragments

当時の Module Blocks と Module Fragments の構文・機能の類似性から、(Function Expressions の Function Declarations 関係のように)Module Expressions と Module Declarations として改称することになったそうです。

参考リンク