- Explicit Resource Management 提案は、ファイルハンドルやネットワーク接続などのリソースのライフサイクルを明確に制御する新しい方法
- Chromium 134 と V8 v13.8 からこの機能を利用可能
- 言語に追加される要素
using および await using 宣言と Symbol.dispose, Symbol.asyncDispose シンボルの導入により、自動クリーンアップの仕組みを提供
DisposableStack, AsyncDisposableStack は複数のリソースを安全にグループ化して解放
SuppressedError は、クリーンアップ中に発生したエラーと既存のエラーをあわせて管理
- この方法はコードの安全性と保守性を大きく高め、リソースリーク防止に効果的
- 従来の try...finally パターンを簡素化し、大規模で複雑なリソース環境でも信頼性の高いリソース処理が可能になる
明示的リソース管理提案の概要
- Explicit Resource Management 提案は、ファイルハンドルやネットワーク接続などのリソースを明確に生成・解放できる新しい方法を導入
- 主な構成要素は次のとおり
using および await using 宣言: スコープ終了時に自動でリソースを解放
[Symbol.dispose](), [Symbol.asyncDispose]() シンボル: 解放(cleanup)動作を実装するためのメソッド
- グローバルオブジェクト DisposableStack, AsyncDisposableStack: 複数のリソースをグループ化して効率的に管理
SuppressedError: リソース整理中に発生したエラーと既存エラーの両方を含む新しいエラー型
- これらの機能は、開発者がきめ細かくリソースを管理し、コードの性能と安全性を向上させることに重点を置いている
using と await using 宣言
using 宣言は同期リソースに、await using 宣言は非同期リソースに使用
- 宣言されたリソースはスコープを抜けると 自動的に Symbol.dispose
** または **Symbol.asyncDispose` が呼び出される
- これにより同期/非同期リソースのリーク問題を減らし、一貫した解放コードを書ける
- このキーワードはコードブロック、for ループ、関数本体の中でのみ使用でき、トップレベルでは使用不可
- 例
- たとえば
ReadableStreamDefaultReader を使う場合、ストリームを再利用するには reader.releaseLock() を必ず呼び出す必要がある
- エラー発生時にこの呼び出しが抜けると、ストリームが永久にロックされる問題が起きる
- 従来の方法
- 開発者は try...finally ブロックを使って、リーダーのロック解除を保証していた
- finally ブロックに
reader.releaseLock() のコードを書く必要がある
- 改善された方法:
using の導入
- 解放動作を含むディスポーザブルオブジェクト(readerResource)を作成
using readerResource = {...} パターンを使えば、コードブロックを抜けた時点で自動的に解放される
- 今後 Web API が
[Symbol.dispose] および [Symbol.asyncDispose] をサポートすれば、別途ラッパーオブジェクトを書かなくても自動管理できる可能性がある
DisposableStack と AsyncDisposableStack
- 複数のリソースを効率的かつ安全にグループ化するために
DisposableStack と AsyncDisposableStack が導入される
- 各スタックにリソースを追加し、スタック自体を解放すると内部のすべてのリソースが逆順で解放される
- 依存関係のある複雑なリソース集合を扱う際のリスクを減らし、コードを簡素化する
- 主なメソッド
use(value): スタックの最上部にディスポーザブルリソースを追加
adopt(value, onDispose): ディスポーザブルではないリソースに解放コールバックを紐づけて追加
defer(onDispose): リソースなしで解放動作だけを追加
move(): 現在のスタックの全リソースを新しいスタックへ移し、所有権を移転可能にする
dispose(), asyncDispose(): スタック内の全リソースを解放
サポート状況と活用可能な時期
- Chromium 134、V8 v13.8 以降で明示的リソース管理機能を利用可能
- 今後はさまざまな Web API との互換性拡大も期待される
4件のコメント
await using data = await fn()awaitが左辺と右辺の両方に現れる奇跡https://typescriptlang.org/docs/handbook/…
Hacker Newsの意見
この提案は「関数の色」問題に似た感じがする。同期待機関数と非同期待機関数の区別が、あらゆる機能にまで入り込み続ける。たとえば Symbol.dispose と Symbol.asyncDispose、DisposableStack と AsyncDisposableStack の例を見ればわかる。Java が仮想スレッド(virtual threads)へ進んだ理由には納得している。JVM に複雑さを追加することで、アプリケーション開発者、ライブラリ作者、デバッガの負担を減らす選択だと思う
非同期を隠してしまうと、コードの流れを理解するのがむしろ難しくなるので同意しない。リソースが非同期に解放されるのか、ネットワーク問題のような外部要因の影響を受ける可能性があるのかも知っておきたい
最近は多くの言語で「すべてのコードを非同期で書くのが常識」という風潮が本当にうんざりする。Purescript は Eff(同期エフェクト)や Aff(非同期エフェクト)でコードを書き、呼び出し時点で選べる唯一の例だと思う。構造化並行性(Structured concurrency)は格好いいが、実際には構造化並行性を得るための構文的な作業というより、サーバー上で複数のトップレベルのリクエストハンドラを持つための作業に近い。並列処理を簡単にするための手段にすぎない
JVM でどう実装されているのかは知らないが、一般にマルチスレッドは本当に直感的に扱いにくい技術だ。さまざまな競合状態、デッドロック、ライブロック、飢餓、メモリ可視性の問題などを扱う本が数多くある。これに比べれば、単一スレッドの非同期プログラミングのほうがはるかに負担が少ない。関数の色問題を受け入れるほうが、マルチスレッドアプリで「Heisenbug」をデバッグするよりは苦痛が少ない選択だ
Java がその選択をしたことが本当にうれしい
通常実行と非同期関数が互いに閉カルテシアン圏(closed Cartesian categories)を形成しているからだ、という説明。通常実行の圏は非同期の圏に直接埋め込める。すべての関数はカテゴリー(つまり関数の色)を持ち、ある言語はそれをより露骨に表す。これは言語設計上の選択であり、圏論はスレッディングを超えて強力に活用できる。Java とスレッドベースのアプローチは同期の問題に直面するが、これが特に難しい。JavaScript はモナディックな圏の中でも特に Continuation-passing 方式に制限を設けている
defer 関数を使った using の使用例を見たとき、とても新鮮に感じた。他の多くの人にはすでに直感的かもしれないが、言及する価値はあると思う
using提案に含まれる DisposableStack と AsyncDisposableStack を使うと、コールバック登録を標準でサポートできる。usingはブロックスコープなので、スコープをまたぐ場合や条件付き登録にはそれが必要になる。しかしusing変数はconstのように即座に初期化されなければならず、条件付き初期化はできない。こういうときは関数の先頭で Stack を作り、使用するリソースを defer でそのスタックに積むパターンが必要だ。必要な場合にだけ解放時点を関数レベルへ簡単に調整できるgolang に似た感じ
本当に良いアイデアだと思うが、<p>Web API ストリームなどで [Symbol.dispose] と [Symbol.asyncDispose] の統合が将来的に可能だとしても、近い将来は一部の API やライブラリだけがこの機能をサポートし、残り(大半)はサポートしない状況になるだろう。結局、「using」と try/catch を混ぜるか、あるいはすべてのコードで try/catch を使って理解しやすさを優先するかというジレンマになる。これによってこの機能が「実用的には使えない」という評判を得る危険がある。実際の問題を解決する良い設計なのに、導入が難しくなるかもしれない点が惜しい
こうした機能をサポートしない API に対しては DisposableStack を使って
usingを適用できる。複数のリソースを一緒に扱う場合でも、try/catch よりずっと単純になる利点がある。ランタイムさえ対応していれば、既存リソースのアップデートを待たずにすぐ使えるJavaScript ではこういう状況が 15 年間繰り返されてきた。新しい言語機能は Babel のようなコンパイラに先に導入され、その後で仕様に入り、最後に安定した API やブラウザへ適用されるまで 3〜4 年かかることが多い。開発者はどうせ小さなラッパー(wrapper)で Web API を包むことに慣れているし、ポリフィルよりラッパーのほうがよい場合も多い。有用な新しい言語機能が生まれても、「使いにくそうだ」と思ったことは一度もない
実際、多くの機能はすでにポリフィルで実装されており、NodeJS エコシステムの大半がこのパターンを使っているし、利用者はトランスパイラで文法だけ合わせて使ったりもする。昨年この件に関する発表を準備していたとき、NodeJS や主要ライブラリにはすでに Symbol.dispose をサポートする API がかなり多いことに気づいた。フロントエンドではライフサイクル管理システムがあるのであまり使われないかもしれないが、一部の状況では依然として有用だ。テストライブラリやバックエンドでは十分に広がると思う
TC39 は Rust の trait / protocol のような根本的な言語機能にも注力する必要がある。Rust では新しい trait の定義と実装が比較的容易だが、動的言語であり固有シンボルを持つ JS なら、もっと簡単に導入できるはずだ。orphan rule のような欠点はあるが、はるかに柔軟な構造へ発展できる
JavaScript の世界では、普通はポリフィルで解決する
C# を思い出す。IDisposable と IAsyncDisposable によって、ロック管理、キュー、一時スコープ管理などの抽象化に非常に有用だ
提案の作者が Microsoft 出身なので、構文が C# と似た形に決まった。関連する GitHub issue でも一貫した文脈がある
基本的には C# から借用したデザイン。元の提案は Python の context manager、Java の try-with-resources、C# の using statement なども参照している。using キーワードと dispose フックメソッドはかなり大きなヒントだ
JavaScript が下位互換性の維持を重視するのは理解できるが、
[Symbol.dispose]()という構文は不自然に感じる。配列にメソッドハンドルがあるようにも見えてしまう。この構文が何なのか、もっと知りたいと思ったオブジェクトリテラルで左辺に角括弧で囲んだ動的キー(dynamically computed property)を使う書き方は、ES6 以降 10 年近く使われているという説明。また、シンボルは文字列で参照できないため、動的キーとメソッド短縮記法を組み合わせている。根本的に新しい構文ではないという考え
しっかりした資料とともに、これは既存オブジェクトにシンボルキーを割り当てる方法に由来しており、自然な流れだという説明
他のユーザーがすでに何であるかは説明していたが、なぜそうなのかの説明はなかったように思う。メソッド名に Symbol を使えば、既存メソッドと衝突せず新しい API であることを保証できる。誤ってクラスが disposable として扱われるのを防ぐ効果もある
dynamic property access の概念への言及。オブジェクトプロパティはドット(.)または角括弧([])でアクセスでき、文字列とシンボルの両方をサポートする。シンボルは固有オブジェクトとして比較され、「[Symbol.dispose]」のような well known symbol(よく使われる特別なシンボル)によって拡張性が保証される。Python の dunder メソッドに似た概念という説明もある
この構文はもう何年も使われている。JavaScript の iterator も同じ方式で、導入されたのはほぼ 10 年前だ
リソース管理、特にレキシカルスコープが特徴である場合に、JS に構造化並行性を導入しようとしてきた理由の紹介。関連する構造化並行性ライブラリも共有
Bun 1.0.23 以降ではすでにこの機能をサポートしている。実験的に試せる
こんなに複雑なコードスタイルで、どうやってプログラムの実行フローを理解し制御できるのか、まったくわからないという疑問
まさにそこが本質だ。Web 開発の 90% は役に立たないか誰も望んでいないアップグレードで、その結果生じた問題を残り 10% の時間で修正するのが現実だ。低い確率で昔書かれたコードを誰かが見なければならない状況が来るが、そこでバグを新人の入門課題として残しておくというアイデアがおすすめだ。20 年前のレガシーシステムでさえいまだに使われている現実もある
例として示されたコードには深刻な構文エラーが多く、実際の JS とはかなり離れている。そして JS 開発者はこういうふうに while、promise chain、finally などを混在させることはあまりなく、await か適切な例外処理構造を使うのが一般的だ。よく設計されたライブラリでは何段階ものハンドラを重ねず、DisposableStack を活用してもっと簡潔に書ける。最近では即時実行 async 関数さえ不要な場合が多い
その言語でプロとして働き、その言語のキーワードの意味や動作に慣れれば、コードは自然に理解できるようになる。Haskell プログラマも同じように慣れる
HN でコードを埋め込むときは、各行に 2 文字以上のインデントが必要だ。(コードの理解が難しいという点には同意する)
インデントが助けになる、という簡潔な助言
なぜ匿名(anonymous)クラスのデストラクタにしなかったのか、あるいは Symbol 以外の構造を使わなかったのか気になる。2 つの Symbol(同期/非同期)が存在すると、抽象化が漏れる問題があるのではないかという指摘
デストラクタには予測可能な(cleanup が明確な)動作が必要だが、進化した GC(garbage collector)はこのパターンと相性がよくない。現代の言語は scope(スコープ)ベースの cleanup をサポートし、高階関数、特別なフック、コールバック登録などさまざまな方法で実装している。Python も初期にはデストラクタベース(refcount GC)だったが、限界のため context manager が導入された
他の言語のデストラクタは GC のタイミング次第で動作するため信頼しにくい。一方、dispose メソッドは変数スコープが終わるときに明確に呼び出されるので、ファイルを閉じる、ロックを解放するなどにおいて予測可能だ。Symbol ベースのメソッドは既存機能との衝突を避けられ、通常はライブラリ開発者だけが気にすればよい。同期/非同期の区別は明確である必要があり、
await using a = await b()のような少し見慣れない構文が必要になることもあるGC 言語ではデストラクタの同期呼び出しが難しく、その多くは非決定的な動作になる。JS には WeakRef と FinalizationRegistry があるが、Mozilla でさえ予測不能として使用を推奨していない
この方式はクラスインスタンスではない対象にも使える点が強みだ
JavaScript には匿名プロパティ(anonymous property)という概念がないので、質問自体が曖昧に感じられる。この方法以外に代替案はないという主張
提案文の最初の例は、try/finally でロックを安全に解放するコードの例だった。こうしたパターンは長時間実行が必要な状況でだけ重要なのか、ブラウザや CLI 環境でエラーによってプロセスが終了する場合でもロックが解放されるのか気になる
仕様では、ブロックの実行が正常終了しても、例外・分岐・脱出で終わっても、必ず dispose が実行されるとされている。つまり using でも try/finally でも同じだ。強制終了(プロセスの強制終了)は仕様の範囲外なので ECMAScript は関与しない。例の stream は JS 内部オブジェクトなので、インタプリタが消えればロックという概念自体が意味を失う。もし OS リソース(メモリ、ファイルなど)なら、通常は OS が一括で後始末するが、動作はプラットフォームごとに異なる
ブラウザの Web ページは見方を変えれば非常に長時間動作するアプリケーションだ。サーバープロセスより長く動くことさえある。エラーが起きてもページ自体が死ぬわけではなく、例外を含むエラー処理は明確なルールに従って finally で処理される。NodeJS ではデフォルトではエラー時にプロセスが終了するが、サーバーの状況によっては別の処理も一般的だ。つまり finally で解放関数が必ず呼ばれる
これまでリソースなんてまったく気にせずにやってこれたじゃないか。急にどうしたんだ?