TypeScript コンパイラへの Go の defer 機能追加

2026/08/03 5:12

TypeScript コンパイラへの Go の defer 機能追加

RSS: https://news.ycombinator.com/rss

要約

Japanese Translation:

この文章は、Go の

defer
文を TypeScript に統合することは、アーキテクチャ上の不整合および意味論的な衝突のために根本的に不適切であると論じています。Go はセマフォや「エラー-as-値」といったパターンに依存する一方、TypeScript には厳密な同等物が欠如しており、開発者はぎこちない
try/finally
ブロックやクロージャによる回避策を採用せざるを得なくなっています。著者は、AST から AST への変換を利用して
defer cleanup()
などの構文をクロージャを用いたスタックベースのメカニズムに変換するカスタム
defer
を実験的にコンパイラにハッキングしたものの、実装には深い複雑性が露呈しました。特定の制約には、呼び出し元(callee)、受領者(receiver)、引数を直ちに捕捉すること;前の呼び出しが例外を投げてもすべての deferred 呼び出しが実行されることを保証すること(元のエラーを維持し、複数のエラーを
AggregateError
を通じて集約すること);
defer await
を拒否すること;関数体内では
defer
文が呼び出し可能な式であるべきだが生成関数内ではそうでないことを検証することなどが挙げられます。これらのニュアンスがありながらプライベートな TypeScript フォークにおいて利用可能な MVP が存在するにもかかわらず、著者はこの機能を TypeScript に強制させることは実用的ではないと結論づけます。代わりに、ECMAScript の明示的なリソース管理(Explicit Resource Management)案のような公式の標準化努力を待つことを推奨しており、この提案は
using
文と disposable オブジェクトを用いて、Go 方式のエラー処理を直接移植するのではなく、より清潔な解決策を提供します。

本文

TypeScript コンパイラへの Go 風
defer
構文追加の実装検証

Go の

defer
構文を TypeScript コンパイラ(
tsc
)に追加する難易度を検証した結果、実装が完了した時点ですでに**「おそらく存在すべきではない」と確信しました**。

Go と TypeScript の
defer
の比較

Go の挙動

Go では、

defer
を使用して関数終了直前のコード実行を遅延させます。主にリソースの確保と解放を束ねる目的で使用されます(例:セマフォ)。

func withSemaphore(ctx context.Context, sem *semaphore.Weighted) error {
    if err := sem.Acquire(ctx, 1); err != nil {
        return err
    }
    defer sem.Release(1) // 終了時に実行
    // ... プロテクトされた処理
    return nil
}

TypeScript の現状

TypeScript には

defer
と同等の構文は存在しません。代わりに
try/finally
を使用することになります。

async function readFile(path: string) {
    await sema.acquire();
    try {
        // ... リソースの利用
    } finally {
        sema.release(); // 明示的に記述が必要
    }
}

このアプローチは冗長であり、コードが「綺麗」ではありません。

コンパイラ実装の可能性と課題

defer
構文を実装することは技術的に行えます。TypeScript コンパイラに Go に似た振る舞いを JavaScript コードとして出力させることが可能です。

目標のシンタックス

async function readFile(path: string) {
    await sema.acquire();
    defer sema.release(); // 新しい構文
    // ... use resource
}

実装アプローチ:AST 変換

TypeScript コンパイラ(

tsc
)は静的解析エンジンであり、動的な言語への型チェックや低レイテンシ IDE サポートを処理しています。
defer
を追加する場合、深い型分析ではなく、既存の「シンタックス X → シンタックス Y」の変換機能を利用すれば十分です。

入力 AST:

function f() {
    defer cleanup();
    work();
}

出力 JavaScript:

function f() {
    const __defers = [];
    try {
        __defers.push(() => cleanup());
        work();
    } finally {
        // Pop して実行する
    }
}

パーサーとチェックの実装ステップ

  1. シンタックス定義:
    DeferStatement
    をリストに追加し、単一の式オペランドを持つ文として定義します。
  2. コンテキストチェック: 以下の条件を確認します。
    • defer
      文が関数本体内部にあること。
    • オペランドが実行可能な呼び出し式(Call Expression)であること。
    • ジェネレーター関数内では使用不可であることを保証します。
func (c *Checker) checkDeferStatement(node *ast.Node) {
    c.checkGrammarStatementInAmbientContext(node)
    
    // defer はそのを含む関数のライフサイクルと結びついている
    fn := ast.GetContainingFunction(node)
    if fn == nil || fn.Body() == nil || !ast.IsBlock(fn.Body()) {
        c.grammarErrorOnNode(node, diagnostics.Defer_statements_can_only_be_used_inside_function_bodies)
    } else if ast.GetFunctionFlags(fn) & ast.FunctionFlagsGenerator != 0 {
        c.grammarErrorOnNode(node, diagnostics.Defer_statements_cannot_be_used_in_generators)
    }
    
    // 呼び出しのみをサポートしている
    expression := ast.SkipParentheses(node.Expression())
    if !ast.IsCallExpression(expression) {
        c.grammarErrorOnNode(node.Expression(), diagnostics.The_operand_of_a_defer_statement_must_be_a_call_expression)
        c.checkExpression(node.Expression())
        return
    }
    
    // 通常の呼び出しチェックを再利用(実行可能な callee、引数の型など)
    c.checkExpression(expression)
}

注意: 実際のトランスフォームコードは冗長が多いためここでは省略します。

defer
の動作メカニズム

Go の挙動を再現するためには、呼び出し側(this)、リセーバー、引数の値を即時にキャプチャする必要があります。

キャプチャの必要性

let x = 1;
defer console.log(x);
x = 2; // defer が実行されたら 1 を出力しなければならない

メソッド再定義への耐性

メソッドが後に再定義されても、初期の状態を維持します。

const logger = {
    log(message: string) {
        console.log("old:", message);
    },
};

defer logger.log("hello");

// 以降はすべて変化します
logger.log = (message) => {
    console.log("new:", message);
};

// deferred call は依然として元のメソッドを呼び出します。

スティックとスタック処理

  • defer
    を含む関数には小さなスタックが割り当てられます。
  • defer
    文は到達時にクロージャをプッシュします。
  • 関数終了時、スタックは**逆順(LIFO)**で排出されます。
  • 登録タイミング: 関数開始時ではなく、実行が
    defer
    に到達した時に発生します。

コード変換例

入力:

async function readFile(path: string) {
    await sema.acquire();
    defer sema.release();
    return await fs.readFile(path, "utf8");
}

出力(内部実装):

async function readFile(path) {
    const stack = [];
    try {
        await sema.acquire();
        const receiver = sema;
        const method = receiver.release;
        
        // クリーンアップは defer 文に到達した場合のみ登録される。
        stack.push(() => method.call(receiver));
        return await fs.readFile(path, "utf8");
    } catch (error) {
        // クリーンアップが実行できるように、元のエラーを保存する。
    } finally {
        // 登録されたコールバックを逆順で実行する。
        // async 関数の場合、各クリーンアップ処理は次の処理に移る前に await される。
    }
}

制限事項:
defer await
は非対応

  • defer await
    の独自のセマンティクスを実装する代わりに、エラーとして拒否しています(誤推測を防ぐため)。
  • async
    関数内のクリーンアップは、順次
    await
    されます。

エラー処理の課題

TypeScript ではエラーが例外(制御フロー)のため、Go のように値を返すわけではありません。コンパイルされたコードでエラーが発生した際、以下の 3 つのルールに従います:

  1. 以前のエラーのスローまたはすべての
    deferred call
    の実行は続行されます。
  2. 関数本体の元のエラーは保持されます。
  3. 複数のエラー発生時は
    AggregateError
    で報告されます。

例:

async function f() {
    defer asyncCleanup();
    throw new Error("body");
}
// asyncCleanup() も reject/throw すると、f() は AggregateError で reject される。
// AggregateError([
//     Error("body"),
//     cleanupError
// ]);

TypeScript に

defer
を実装する過程で生じる多くのエッジケース(集約、優先順位、非同期ポリシーなど)は、Go のエラーセマンティクスには存在しません。Go の
defer
は自然だが、TypeScript には不向き
であることが確認できました。

代替案:ECMAScript Explicit Resource Management

希望を失う必要はありません。同じ課題を異なるアプローチで解決する標準規格(ES2024+)があります。

実装例:
using
ステートメント

async function readFile(path: string) {
    using _ = await acquirePermit(sema); // リソース解放が明示的
    return await fsReadFile(path, "utf8");
}

// ヘルパー関数
async function acquirePermit(sema: Sema): Promise<Disposable> {
    await sema.acquire();
    return {
        [Symbol.dispose]() {
            sema.release();
        },
    };
}

理想的な構文(現時点では未対応)

TypeScript フォークのブランチには

defer
の MVP 実装がありますが、以下の理想構文はまだサポートされていません。

// サポートされていない!
async function readFile(path: string) {
    using await acquirePermit(sema); 
    return await fsReadFile(path, "utf8");
}

同じ日のほかのニュース

一覧に戻る →

2026/08/03 1:26

Show HN: Kakehashi – Linux ARM で macOS バイナリを実行するための実験的なユーザースペース

## Japanese Translation: Kakehashi は、JIT コンパイルや Apple の専用 SDK に依存せず、Linux aarch64 上で実際の macOS ARM64 ゲストを実行するためのオープンソースで CLI ファーストのユーザースペース翻訳層です。中心となるクリート(`kh-loader`、`kh-runtime`、埋め込まれた `libSystem.B.dylib`)を中心に構成され、システムコールを翻訳するとともに、ゲストのファイルシステムをホストに `/Volumes/linux/…` を介して橋渡しします。具体的には、ゲストの `/usr/local/bin` をホストのバイナリに、`/etc/ssl/cert.pem` を CA バンドルにマッピングします。インストールは `cargo install kakehashi`(ソース:`crates/kh-cli`)で行い、事前に `kh bottle ensure` でボトルを確保します。ツールの追加は `kh install`、実行は `kh run` によって行われます(例:マルチスレッド圧縮用の `kh run 7zz -- -mmt=4` や、単に `kh run curl --` など)。Linux aarch64 ベアメタル、VM、Docker/Colima(ヘルパーがアーティファクトを `.tmp/kh-out/` に出力)上で動作し、Rust 1.88 以上(Linux aarch64)、コンテナの種類に応じて 4 KiB または 16 KiB のページサイズに対応します。ベンチマークの結果では、Linux 側のマルチファイル 7-Zip 圧縮とネイティブ実行を比較した場合の全実行数のギャップは約 5.2 倍ですが、単一ファイルまたは圧縮負荷の重いワークロードではオーバーヘッドは約 1.1〜1.2 倍に留まります。Darwin クライアントツールを高価な macOS ランナー($0.062〜$0.102/分)ではなく、低価格な Linux ARM64 ランナー($0.005/分)上で実行できるため、パフォーマンスのオーバーヘッドがあっても Kakehashi は多くの場合で費用対効果に優れています。Apache 2.0 ライセンスの下にあり、Darling から派生していない本ツールは、自動化された CLI ワークフローのためにエコシステムを橋渡しする無料の代替手段を提供します。

2026/07/28 23:21

メモTaking とパーソナルナレッジ管理

## Japanese Translation: 本稿の主要な論旨は、ブレンnan ケネス・ブラウンの記事に対し、ノートツール「Obsidian」に独自の知的価値を誤って帰属させ、不均衡な見解を示していることを批判しています。著者は、ソフトウェアが整理を助けることは事実だが、画期的なアイデアそのものの源泉ではないと主張します。証拠によると、ブラウンは Obsidian が複雑なシステムであるかのように誤って描写しており、実際には 1994 年頃の技術に準じるような個人的なウィキとして機能しています。この分析では、PARA やニコラス・ルーマンが使用した歴史的なゼッテルkasten メソッドなど、確立された枠組みを参照して議論の文脈を設定し、そのようなツールは人間の創造性を置き換えるのではなくそれを支援するに過ぎないと指摘します。さらに、Obsidian のダウンロード数が約 75 万回に達しているにもかかわらず、それは 460 億ドル規模の巨大な業界内で運営されており、その現在の影響は限定的であることを示唆しています。この批判は、世界を変えるような貢献を直接ソフトウェアに帰属させることは誤った結論と不確実な引用につながることを警告しています。結局のところ、ユーザーはこのツールを独自性の源泉ではなく、個人的な解決策のための基盤として認識するべきです。 ## Text to translate: The central argument critiques Brennan Kenneth Brown's article for presenting an unbalanced view that wrongly attributes unique intellectual value to the note-taking tool Obsidian. The author asserts that while software facilitates organization, it is not the source of groundbreaking ideas itself. Evidence shows Brown mischaracterizes Obsidian as a complex system when it functions essentially as a personal Wiki, comparable to technologies from 1994. This analysis contextualizes the debate by referencing established frameworks like PARA and the historical Zettelkasten method used by Niklas Luhmann, noting that such tools merely support human creativity rather than replacing it. Furthermore, despite Obsidian having roughly 750,000 downloads, it operates within a vast $46 billion industry, suggesting its current impact is limited. The critique warns that attributing world-changing contributions directly to the software leads to flawed conclusions and inconclusive citations. Ultimately, users should recognize these tools as foundations for personal solutions rather than engines of original thought.

2026/08/03 5:26

FamilyWild を用いたホスト間の X11 サーバー共有

## Japanese Translation: 2026 年 8 月 2 日、隔離環境(コンテナや chroots など)内または非転送された SSH 接続上でグラフィカルな X11 アプリケーションを動作させる際に生じる「Authorization required, but no authorization protocol specified」というエラーを解決するための方法が詳述されました。根本原因は、`.Xauthority` クッキーが family と hostname の双方で鍵付けされており、クライアントが自分のマシン名と一致しない hostname を持つクッキーを拒絶する点にあります。 解決策は、クッキーの family フィールドの最初の 2 バイトを `0100`(`FamilyLocal`)から `0xffff`(`FamilyWild`)に書き換えることです。これには以下のコマンドを使用します:`xauth nlist :0 | sed 's/^..../ffff/' | xauth -f /tmp/portable.Xauthority nmerge -`(`:0` を `$DISPLAY` に置き換えてください)。family を `FamilyWild` に変更することで、クッキーは任意の hostname に対して有効となり、hostname が不一致のクライアントからの接続も可能になりつつ、ホストベースのアクセス制御を完全に無効にすることなく済みます。 これを使用するには、生成された `/tmp/portable.Xauthority` ファイルを bind-mount または SCP でクライアント環境に移動し、`$XAUTHORITY` 変数を指すように設定します。ただし、厳格なセキュリティ上の注意が必要です:`FamilyWild` クッキーはローカルなものよりも特定の情報が少ないため、ソケットアクセスがありファイルを閲覧できるあらゆるユーザーが表示器に接続できるようになります。そのため、ファイルのパーミッションは必ず 0600 を維持し共有マシンにはコピーを残すべきではありません。このアプローチは、ホストベースのセキュリティを完全に無効にする `xhost +` の使用や、全クッキーをクリアしつつ無効なエントリを残そうとする危険な方法よりも優先されます。著者はこのトリックを、特別に非特権 LXC コンテナへの X11 転送のために適用しています。

TypeScript コンパイラへの Go の defer 機能追加 | そっか~ニュース