世界を動かす技術を、日本語で。

JavaScriptの新しいスーパーパワー:明示的リソース管理

概要

  • Explicit Resource Management 提案は、リソース管理を決定的かつ明示的に行う新機能をJavaScriptに導入
  • using/await using 宣言により、リソーススコープ終了時に自動でクリーンアップ処理を実行
  • DisposableStack/AsyncDisposableStack で複数リソースの効率的な一括管理が可能
  • SuppressedError でリソース解放時のエラーと本体エラーの両方を保持
  • Chromium 134/V8 v13.8/Chrome 134/Firefox 134でサポート、Safari・Node.js未対応

明示的リソース管理(Explicit Resource Management)提案の解説

提案の概要と追加要素

  • Explicit Resource Management は、ファイルハンドルやネットワーク接続などのリソースのライフサイクルを明示的かつ決定論的に管理することを目的とする提案
  • using および await using 宣言を追加し、スコープ終了時に自動で dispose メソッドを呼び出すことを保証
  • Symbol.dispose および Symbol.asyncDispose シンボルにより、クリーンアップ処理を明確化
  • DisposableStack (同期用)・ AsyncDisposableStack (非同期用)というグローバルオブジェクトで、複数のディスポーザブルリソースを一括管理
  • SuppressedError 型で、リソース解放時のエラーと本体処理中のエラーの両方を保持・管理

using/await using 宣言の特徴

  • using宣言 は同期リソース用で、スコープ終了時に Symbol.dispose を自動呼び出し
  • await using宣言 は非同期リソース用で、Symbol.asyncDispose をawait付きで自動呼び出し
  • これにより、同期・非同期リソース両方のリーク防止とコード品質向上を実現
  • using/await using キーワードはブロック内(関数・for文など)でのみ利用可能、トップレベルでは不可
  • 例:ReadableStreamDefaultReaderreader.releaseLock() を自動で呼び出すことで、ストリームのロック解除忘れによるバグを防止

典型的な利用例と従来の課題

  • 従来は try...finally でリソース解放処理(例:releaseLock())を保証する必要があった
    • 例:
      try {
        // 読み取り処理
      } finally {
        reader.releaseLock();
      }
      
  • 今後は、using でラップしたディスポーザブルオブジェクトを使うことで、手動の解放忘れを防止
    • 例:
      using readerResource = {
        reader: response.body.getReader(),
        [Symbol.dispose]() { this.reader.releaseLock(); }
      };
      // 読み取り処理
      // スコープ終了時に自動でreleaseLock()実行
      

DisposableStack / AsyncDisposableStack の活用

  • DisposableStack および AsyncDisposableStack は、複数のリソースを一括でスタック管理できる新しいグローバルオブジェクト

  • スタックにリソースやクリーンアップ処理を追加し、スコープ終了時や明示的なdispose呼び出しで逆順に解放

  • 依存関係がある複数リソースの管理・解放順序を自動化

    • 主なメソッドと用途:

      • use(value):ディスポーザブルリソースをスタックに追加
      • adopt(value, onDispose):非ディスポーザブルリソースと解放コールバックをスタックに追加
      • defer(onDispose):コールバックのみをスタックに追加(リソース非依存のクリーンアップ用)
      • move():スタック内の全リソースを新しいスタックへ移動、所有権移譲
      • dispose()/disposeAsync():スタック内リソースを逆順で解放
    • 例:

      using stack = new DisposableStack();
      stack.use(readerResource); // リソース追加
      stack.dispose(); // 逆順で解放
      

SuppressedError の役割

  • SuppressedError は、リソース解放時に発生したエラーと、もともと発生していたエラーの両方を保持するエラー型
  • 例:本体処理のエラーと解放処理のエラーが同時に発生した場合、どちらも追跡・デバッグ可能

サポート状況

  • Chromium 134 および V8 v13.8 以降で実装済み
    • Chrome :バージョン134以降でサポート
    • Firefox :バージョン134以降でサポート
    • Safari :未サポート
    • Node.js :未サポート
    • Babel :サポートあり

まとめ

  • Explicit Resource Management により、JavaScriptでのリソース管理がより堅牢・効率的・保守性の高いものになることを実現
  • using/await usingDisposableStack/AsyncDisposableStackSuppressedError などの導入で、手動管理の負担やバグリスクを大幅に削減
  • 今後のWeb API(例:Streams)へのシンボル統合も期待されるため、最新動向の確認と活用推進が重要

Hackerたちの意見

誰か、なんで(匿名)クラスのデストラクタを使わなかったのか説明してくれない?特に、シンボル以外の特別なオブジェクトキーを使うとかさ。非同期用の別のシンボルもあるし、これって漏れやすい抽象化になっちゃうんじゃない?

このアプローチは、クラスのインスタンスじゃないものにも使えるからね。

他の言語のデストラクタは、通常オブジェクトがガベージコレクトされるときに使われるんだ。でも、それにはいろんな問題が伴うから、最近はこのパターンを避けることが多いんだよね。一方で、disposeメソッドは変数がスコープを出るときに呼ばれるから、もっと予測可能なんだ。例えば、メソッドが返る前にファイルが閉じられたり、ロックが解除されたりするのを信頼できる。JavaScriptは他の場所でも同期と非同期を明確にしてるし、ここも例外じゃない。メソッドはdisposeが完了するのを待たなきゃいけないから、もしdisposeが非同期なら、メソッドも非同期にしなきゃならないんだ。ただ、慣れてないとawait using a = await b()みたいにダブルawaitになるのはちょっと面倒かもね。シンボルを使うのは、時間をかけて追加された他の機能と同じで、イテレーターとかもそうだし、後方互換性を保ちながらサポートを追加するいい方法なんだ。シンボルを扱うのは主にライブラリの作者だけで、典型的なアプリ開発者は直接触れる必要はほとんどないよ。

ガベージコレクトされる言語では、デストラクタはほとんどの場合同期的に呼び出せないんだ。VMがまずオブジェクトにアクセスできないことを確認しなきゃいけないからね。だから、あまり決定的に動作しないし、JS VMの内部を露呈しちゃう。JSにはすでにWeakRefやFinalizationRegistryがあるし。https://waspdev.com/articles/2025-04-09/features-that-every-... https://waspdev.com/articles/2025-04-09/features-that-every-... でも、Mozillaですらそれらを使うことはあまり推奨してないよ。予測不可能で、エンジンによって動作が異なることがあるからね。

JavaScriptは未開発だからね。

デストラクタは決定的なクリーンアップが必要だけど、高度なガベージコレクション(GC)ではそれができないし、効率の観点からもあまりやりたくないんだよね。高度なGCを持つ言語では、コレクション中に呼ばれる「ファイナライザ」があって、これがすごく信頼性が低い(しかも微妙な罠がいっぱいある)んだ。通常はネイティブリソース(FFIラッパー)のための最終手段として使われることが多い。だから、多くの言語はスコープベースのリソースクリーンアップの手段を持っていたり、最終的にそれを成長させたりしているんだ。例えば、 - 高階関数ベース(Smalltalk、Haskell、Ruby) - 専用のスコープ/値フック(Python、C#、Java) - コールバック登録(Go、Swift) [1]: Pythonは元々、参照カウントGCのおかげでデストラクタを使ってたけど、参照カウントされていない実装や参照カウントのサイクル、ロックのようなリソースにガードがないこと(明確なユーティリティがないのにそれを追加したくない)から、コンテキストマネージャが導入されたんだ。

JavaScriptには匿名プロパティなんて存在しないよ。君の質問は意味がわからない。他に何が考えられるっていうの?

C#を思い出すなぁ.. C#のIDisposableとIAsyncDisposableは、ロック処理やキューのメカニズム、一時的なスコープの扱いなど、実際にきれいに抽象化すべきものを書くのにすごく役立つよね。

これは基本的にC#からの流用で、元の提案はそれを隠していないし、PythonのコンテキストマネージャやJavaのリソースを使ったtry、C#のusing文やusing宣言をすべて引用しているよ。そして、usingがキーワードで、disposeがフックメソッドっていうのはかなりのヒントだね。

それは提案の著者がMicrosoft出身で、C#と違う構文に見える反対提案を何度も却下してきたからだよ。 https://github.com/tc39/proposal-explicit-resource-managemen... https://github.com/tc39/proposal-explicit-resource-managemen... https://github.com/tc39/proposal-explicit-resource-managemen... https://github.com/tc39/proposal-explicit-resource-managemen...

彼らの最初の例は、こういう関数の中でtry/finallyブロックを持つ必要があるってことだね:function processData(response) { const reader = response.body.getReader(); try { reader.read() } finally { reader.releaseLock(); } } だから、reader.read()がエラーを投げてもロックが解除されるようになってる。これは長時間実行されるプロセスにだけ当てはまるのかな?ブラウザ環境やエラーが投げられたときに終了するCLIスクリプトでは、プロセスが終了するときにロックは解除されるの?

仕様には、ブロックが「完了」したとき、つまりどういう形であれ(通常の完了、例外、break/continue文など)、disposeが実行されなきゃいけないって書いてあるんだ。これは「using」にも「try/finally」にも同じことが言えるよ。プロセスが強制終了されたときの動作は、ECMAScript仕様の範囲外だから、その時点でインタープリターはこれ以上のアクションを取れなくなるんだ。だから、何が起こるかは話しているオブジェクトの種類による。記事の例は、ウェブプラットフォームのストリーム仕様からの「ストリーム」について話してる。ここでのストリームは、JSインタープリター内にだけ存在するJSオブジェクトなんだ。JSインタープリターが消えたら、ロックがかかっているかどうかを問うのは意味がないよ、だってロック自体が存在しなくなるから。もしOSが割り当てたリソース(例えば、割り当てられたメモリやファイルディスクリプタ)について話しているなら、プロセスが終了するときには一般的にOSが提供するクリーンアップが行われるよ。終了の仕方に関係なく、プロセス自体が何もしなくてもね。でも、もちろん詳細はプラットフォームに依存するけど。

ブラウザのウェブページは、典型的な長時間実行プログラムだよ!少なくともNotionの場合、ブラウザタブは通常、サーバープロセス(次のデプロイまで数時間)よりもずっと長く(数日から数週間)生きてるからね。サーバーのようにイベントループがあって、複数のサブプロセスを持ってることが多いから、完了まで実行するCLIツールとは全然違うよ。そして、エラーはウェブページを終了させない。未処理のエラーの実行順序は明確に定義されてる。エラーはコールスタックを上に戻りながらcatchやfinallyブロックを実行して、イベントループに戻ると、システムによって「未捕捉の例外」(同期コンテキスト)や「未処理の拒否」(非同期コンテキスト)ハンドラ関数に送られることが多いよ。NodeJSでは、デフォルトのエラーハンドラはプロセスを終了させるけど、自分の動作に置き換えることもできるのが長時間実行されるサーバーでは一般的だね。要するに、はい、これは機能するよ。終了ハンドラはスタックの一番上で呼ばれるから、スタックがfinallyブロックを通って戻った後にね。

どうやってこんなコードを書いて、プログラムの実行について何かを考えたり制御したりできるのか理解できないよ :) async (() => (e) { try { await doSomething(); while (!done) { ({ done, value } = await reader.read()); } promise .then(goodA, badA) .then(goodB, badB) .catch((err) => { console.error(err); } catch { } finally { using stack = new DisposableStack(); stack.defer(() => console.log("done.")); } });

インデントは助けになるよ。

その言語で生計を立てていて、その言語のキーワードの意味に慣れているからじゃないかな -- 他の人が自分の好きな言語を理解するのと同じように?結局、Haskellで生計を立てている人もいるしね。

それが面白いところなんだけど、実際には何もしなくていいんだよね。ウェブ開発の90%は、誰も頼んでないし感謝もされない方法で「アップグレード」することだから。コードベースがあまり触られないとカビが生えるみたいに、当然のように思われてるんだよね。残りの10%は、その最初の90%から生じる本当の問題を修正すること。もちろん、確率は1.0にはならないから、たまに1年以上前に書いた「何か」を理解する必要が出てくることもある。上司にこのバグは次の新入社員が入るまで残しておくべきだって提案するんだ。そうすれば、いい「飛び乗りポイント」になるし、それまでユーザーは推奨されているワークアラウンドを使って作業を続けられるから。具体的には、VMにWindows XPの海賊版をインストールして、IE6を使って、なんだか説明がつかないまま20年も経った企業合併の後でもまだ存在するレガシーポータルに入るっていう。

LLMはこれだけをやるだけで、君はそれを好きになるよ。もしかしたら好きにならないかもしれないけど、選択肢はないと思う。

つまり、誰かが変数が数字かどうかを判断するパッケージを作ったコミュニティの話をしてるんだよね… それがめちゃくちゃ使われてる。JavaScriptがいくつかの点でこんなに進化したのに、パラメータの型みたいな基本的なことがまだ欠けてるのは本当に驚きだよ。

HNにコードを埋め込むには、各行の先頭に2つ以上のスペースを追加するんだよ: async (() => (e) { try { await doSomething(); while (!done) { ({ done, value } = await reader.read()); } promise .then(goodA, badA) .then(goodB, badB) .catch((err) => { console.error(err); } catch { } finally { using stack = new DisposableStack(); stack.defer(() => console.log("done.")); } }); (インデントはOPが投稿した通りに保たれてるよ – こんな風にコードを書く人がいるのも理解できないけどね :-)

まず最初に、あなたのコードは深刻な構文エラーが満載で、いくつかの部分では有効なJavaScriptにもなっていません。これが私の推測による再構築です: (async (e) => { await doSomething() while (!done) { ({ done, value }) = await reader.read() } promise .then(goodA, badA) .then(goodB, badB) .catch(err => console.log(err)) .finally(() => { using stack = new DisposableStack() stack.defer(() => console.log('done.')) }) })() でも、もっと重要なのは、これは合理的なJS開発者が書くようなものではないということです。 1. awaitとwhile(!done)を混ぜるのは一般的ではなく、実際にこれを必要とするライブラリは想像できません。通常はどちらか一方を使い、ほとんどの場合はawaitだけです: await doSomething() const value = await readFully(reader) 2. すでにAsync IIFEの中にいるなら、promiseチェーンは必要ありません。必要に応じてawaitすればいいだけです。promiseチェーンがコードを短くきれいにする場合を除いて、例えば: const json = await fetch(url).then(r => r.json()) 3. よく設計されたJSライブラリは、あなたが示唆した{good,bad}{A,B}関数のようにpromiseハンドラをスタックすることはありません。通常はコードを書いて、トップレベルの例外ハンドラを持ちます: using stack = new DisposableStack() stack.defer(() => console.log('done.')) try { const goodA = await promise const goodB = await goodA const goodC = await goodB return goodC } catch(e) { myLogErr(e) } // finallyは必要ありません、それがDisposableStackの全てのポイントです 4. もうAIIFEはあまり必要ないので、外側のレイヤーはそのまま消してしまえます。

どんなプログラミング言語でも、意図的にひどいコードを書くことができるよ。

すべては、きちんとフォーマットされていて、ウェブページのテキストエリアだけじゃなくて適切なコードエディタを持つことから始まるよ。そうすれば、そのコードに対するたくさんのエラーノーティスが得られるからね(だって、確実に有効なJSじゃないし =) もちろん、毎日使う言語を実際に知っていることも役立つよ。だから、あのナンセンスを普通のものに書き直すことができるからね。非同期/待機と.then.catchを混ぜるのは馬鹿げてるし、そのwhileループは、全く普通じゃない状況下でスピンループに入るように意図的に書かれたコードを持ってくるつもりじゃない限り、実際のコードベースには絶対に近づけるべきじゃないよ。

正直、C++より悪くないよ。

これは素晴らしいアイデアだけど、: > ストリームのようなWeb APIで[Symbol.dispose]と[Symbol.asyncDispose]の統合が将来的に行われるかもしれないから、開発者は手動のラッパーオブジェクトを書く必要がなくなる。だから、近い将来、いくつかのAPIやライブラリはその機能をサポートしているけど、他の大多数はサポートしていないという状況になるんだ。だから、複雑な「using」ディレクティブとtry/catchブロックの混合でコードを書くか、機能を無視してすべてにtry/catchを使うかのどちらかになる。後者の方が理解しやすいコードになると思う。私はこの機能が「実用的に使えない」という評判を得るリスクが高いと恐れている(今のところはそうだし)、その評判を覆すのは難しいと思う。これは本当に残念なことで、実際の問題を解決するし、デザイン自体もよく考えられているから。

これって、JavaScriptの世界ではポリフィルで解決されることが多いんじゃない?

だからこそ、TC39はプロトコルのような基本的な言語機能に取り組む必要があるんだよね。Rustでは、新しいトレイトを定義して既存の型に実装できる。これには欠点もあるけど(オーファンルールは問題を防ぐけど、膨張を引き起こす)、ユニークなシンボル機能を持つ動的言語では、何かを考え出すのがもっと簡単になるはず。

これが過去15年間のJavaScriptの状況です: 新しい言語機能はまずBabelのようなコンパイラに来て、それから言語仕様に、そして最終的には保守的なNPMパッケージやブラウザの安定APIに採用されます。「コンパイラプラグインとして現れる」から「いくつかのブラウザAPIに採用される」までのプロセスは、しばしば3〜4年かかります。そして「エバーグリーン」ブラウザで利用可能になった後でも、古いエンドユーザーデバイスで保証されるまでには、ポリフィルやもう少し待つ必要があります。開発者は、改善が非常に遅いので、ウェブAPIの周りに小さなラッパーを書くことに慣れていますし、小さなラッパーはポリフィルに比べて悪くない選択肢です。あるいは、ブラウザAPIが典型的な使用パスで面倒なので、もちろん少し違ったものが欲しいと思います。少なくとも、私自身は便利そうな新しい言語機能を見て「わあ、これは使いにくそうだ」と思ったことはありません。

実際には、たくさんのものが前方互換性のあるポリフィルを使ってこれを実装してるよ。例えば、バックエンドのNodeJSエコシステムのほとんどは、これをたくさんサポートしてるし、しばらくの間この機能をかなり効果的に使えてた(構文を処理するためのトランスパイラを使って)。実際、去年この機能についていくつかの講演をしたんだけど、そのリサーチをしてるときに、NodeJS自体や一般的なライブラリの中で、using構文がどこにも実装されていなくても、Symbol.disposeをサポートしてるAPIがどれだけあるかに驚いたよ。フロントエンドのコードではあまり一般的ではないと思うけど、フロントエンドのコードは通常、自分自身のライフサイクルやクリーンアップ管理システムを持ってるからね。でも、いくつかの場所ではまだ役立つと思う。もう少しテストライブラリがこれらのシンボルを実装するのも見てみたいな。でも、バックエンドのコードでのサポートが広まってるから、時間が経てばそれも実現すると思う。

だから、見通しのある未来では、いくつかのAPIやライブラリがその機能をサポートしている一方で、他の大多数はサポートしていない状況が続くよ。ウェブへようこそ。これはJavaScript 1.1が既存のコードが欲しいもののためにシムを使う状況を作り、新しいコードはそれが言語の一部になったから使わなくなった以来、ほぼずっとそうだね。

これに対応していないAPIでも、DisposableStackを使ってusingを使えるよ:using disposer = new DisposableStack; const resource = disposer.adopt(new Resource, r => r.close()); これ、特に複数のリソースがある場合はtry/catchよりも簡単だから、ランタイムが新しい構文をサポートするようになったらすぐに使えるようになるよ。既存のリソースの更新を待つ必要もないしね。

これを試したいなら、Bun 1.0.23+はすでにサポートしてるみたいだよ: https://github.com/oven-sh/bun/discussions/4325

なんてこった。const defer = f => ({ [Symbol.dispose]: f }) using defer(() => cleanup()) これ、さっき思いついたばかりなんだ。他の人には完全に明白だと思うけど、「よくやった」って感じ。でも、やっぱり言う価値があると思ったんだ。

使用ケースによっては、コールバック登録のための組み込みサポートを持つusing提案の一部であるDisposableStackAsyncDisposableStackを使う方が好ましい場合があります。これはスコープブリッジングや条件付き登録に特に必要です。usingはブロックスコープなので、if (condition) { using x = { [Symbol.dispose]: cleanup } } // ここでcleanupが呼ばれます しかし、usingは初期化値を必要とするconstの変種であり、すぐに登録されるため、これは失敗します: using x; // SyntaxError: using missing initialiser if (condition) { x = { [Symbol.dispose]: cleanup }; } これも失敗します: using x = { Symbol.dispose {} }; if (condition) { // TypeError: assignment to using variable x = { [Symbol.dispose]: cleanup } } 代わりに、こう書きます: using x = new DisposableStack; if (condition) { x.defer(cleanup) } 同様に、ブロック内でリソースを取得したいが、クリーンアップを関数レベルで行いたい場合は、関数のトップレベルでスタックを作成し、進むにつれてその中にディスポーザブルやコールバックを追加します。

ゴーランみたいだね。いいね。

リソース管理、特にレキシカルスコープが特徴であることが、私たちの中の何人かがJSに構造化された並行性を持ち込むために取り組んでいる理由です: https://bower.sh/why-structured-concurrency 構造化された並行性を活用するライブラリ: https://frontside.com/effection

JavaScriptは後方互換性を維持する必要があることは理解していますが、Symbol.disposeという構文は私にはとても奇妙に見えます。これは関数のように呼び出される配列のようで、配列にはメソッドハンドルが含まれています。この構文は何と呼ばれていますか?もっと知りたいです。

https://developer.mozilla.org/en-US/docs/Web/JavaScript/Refe... もっと知識のある人がすぐに参加すると思いますが、これは次のように派生したと確信しています: const x = { age: 42 }; x[Symbol.name] = "joe"; // だから、すごく理にかなっています。

オブジェクトプロパティアクセスかな。例えば、myObj["myProperty"] のように。もしそれが関数なら、myObj"myProperty" のように呼び出せます。キーがシンボルだった場合は、myObjtheSymbol のように。

動的プロパティアクセスかもしれませんね。前提は、インデックス構文と通常のドット構文の両方を使ってオブジェクトのプロパティに常にアクセスできるということです。だから object.fooobject["foo"]object["f" + "o" + "o"] と同じです(なぜなら、角括弧の中の値は任意の式にすることができるからです)。もし object.foo がメソッドであれば、object.foo()object ["foo"]() などもできます。通常、キーの式は常に文字列に強制されるので、もし object[2] をやった場合、これは object["2"] と同じになります。しかし、シンボルには例外があり、シンボルは常に参照によって比較されるユニークなオブジェクトの一種です。シンボルはそのままキーとして使うことができるので、例えば const obj = {} obj.foo = "bar" obj[Symbol("foo")] = "bar" console.log(obj) のようにすると、このオブジェクトにはシンボルという特別なキーがあり、通常の"foo"属性もあることがコンソールに表示されます。最後のピースは、特定の「よく知られたシンボル」があり、これは主にオブジェクトの動作を拡張するために使われます。Pythonの__dunder__メソッドのように。Symbol.disposeはその一つで、グローバルにアクセス可能で、常に同じ意味を持ち、後方互換性を壊さずに新しい機能を定義するために使われます。これが役に立てばいいなと思います。もっと質問があれば気軽にどうぞ。

この構文はかなり前から使われてるよ。JavaScriptのイテレーターも同じ構文を使ってて、もう10年近くJavaScriptの一部なんだ。

動的キー(オブジェクトリテラルの左側の角括弧)は、記憶が正しければもう10年近く前からあるよ。https://www.samanthaming.com/tidbits/37-dynamic-property-nam... 例の中にはメソッドの省略形もあるね: https://www.samanthaming.com/tidbits/5-concise-method-syntax... シンボルは文字列で参照できないから、両方を組み合わせることができるよ。基本的に、ここには新しい構文はないんだ。

const o = {} o["foo"] = function(){} o"foo" let key = "foo" okey key = Symbol.dispose ?? Symbol.for('dispose') okey oSymbol.dispose

他の投稿者はこれが「何」であるかを正しく説明してたけど、誰も「なぜ」なのかには答えてなかったね。メソッド名にシンボルを使うことで、このメソッドが以前に定義されたメソッドと区別されるんだ。つまり、メソッド名にシンボルを使うことで(文字列を使わずに)、この新しいAPIで「名前の衝突」が起こることが不可能になるから、クラスが誤って破棄可能としてマークされることがないんだ。

これは関数への記法的な参照だね。もしコードがobj.function()なら、彼らはそれをfunction()として記述してる。もしコードがobjSymbol.disposeなら、彼らはそれを[Symbol.dispose]()として記述してる。Symbol.disposeはシンボルキーだよ。

この提案は「あなたの関数の色は何ですか?」って感じがするね。同期関数と非同期関数の区別がすべての機能に入り込んでくる。ここでも、Symbol.disposeとSymbol.asyncDispose、DisposableStackとAsyncDisposableStackがあるのがわかるよね。Javaが仮想スレッドの道を選んだのは本当に嬉しい(JEP 444、JDK 21、2023年9月)。アプリケーション開発者やライブラリ作成者、人間のデバッガーがさらに複雑になるのを避けるために、JVMに少し複雑さを持ち込むことにしたんだ。

それには反対だな。非同期を隠すと、コードの理解が難しくなるだけで、楽にはならないよ。破棄が非同期で、ネットワークの障害に影響されるかどうか知りたいんだ。

普通のJavaScriptでは問題ないよ。型はダックタイピングだから、結果やプロミスを受け取っても関係ない。こうしたダイナミズムを使って「色の問題」を機能的に回避できるんだ。完全にダックタイプの言語に全体の型システムを追加しようとすると問題が出てくるんだよね。あるいは、この非同期/待機メカニズムをコピーして、無理やりコンパイル言語に押し込もうとするとね。

Javaがその決定を下したことに本当に嬉しいよ。

これは、通常の実行と非同期関数が異なる閉じたカーテシアンカテゴリを形成していて、通常の実行カテゴリが非同期のものに直接埋め込まれるからなんだ。すべての関数には色(つまり、表現できる特定のカテゴリ)があるけど、明示的にする言語は一部だけなんだ。これは言語設計の選択だけど、カテゴリは非常に強力で、スレッドだけに留まらず適用できるんだ。さらに、Javaとスレッドベースのアプローチは同期処理に対処しなきゃいけないから… 難しいんだよね。(JavaScriptはモナディックカテゴリに制限されていて、特に継続を使った呼び出しで表現できるものに限られているんだ。)

今のところ、最先端の状態がほとんどの言語で「全てのコードを非同期で書けばいい、だって同期呼び出しでも最悪の場合は一時的なイベントループを立ち上げられるから」ってのが本当にイライラするよね。私が知ってる中でこの問題をうまく扱ってるのはPurescriptだけだな。Eff(同期効果)やAff(非同期効果)をターゲットにしたコードを書けて、呼び出す時に選べるから。構造化された並行性は素晴らしいけど、私の印象では、構造化された並行性を得るためじゃなくて、主にサーバー内で複数のトップレベルのリクエストハンドラーを持つために、こういう文法的な作業をしてる気がする。恥ずかしいほど並行してる作業だね!

JVMでどう実装されてるかはわからないけど、一般的にマルチスレッドは考えるのが難しいことで有名だよね。レースコンディション、デッドロック、ライブロック、スタベーション、メモリの可視性の問題など、落とし穴についての本が丸々一冊あるくらい。これに比べたら、シングルスレッドの非同期プログラミングは楽勝だよ。「関数の色」を扱う方が、マルチスレッドアプリでハイゼンバグをデバッグするよりはマシだね。

これはusingに対する批判じゃなくて、asyncに対する批判だよね?私の理解では、これによって関数がより色づくわけではないと思うんだけど。

確かに。バーチャルスレッド、構造化された並行性、スコープ付きの値は素晴らしい機能だよね。

はっきり言うと……これは関数の色づけを導入するものじゃないよ。あなたは単に、既存の関数の色づけの影響を指摘してるだけで、Symbol.disposeとSymbol.asyncDisposeという2つの関連するシンボルがあるってことだよ。Symbol.iteratorとSymbol.asyncIteratorがあるのと同じようにね。