Prompt

Rustは末尾再帰最適化はないんだっけ?

結論から言うと、Rustには言語仕様としての「末尾再帰最適化(TCO: Tail Call Optimization)」の保証はないよ!

ここは、情報工学を学ぶ君にとっても非常に興味深い(そして時に議論を呼ぶ)トピックだと思う。Rustがなぜこれを「保証」しないのか、その理由と現状を解剖してみよう。


1. なぜ Rust には TCO がないのか?

多くの関数型言語(HaskellやElixir、Schemeなど)では、末尾再帰をループに変換することが仕様で決められているけれど、Rustはあえてそれを避けているんだ。

決定的な理由:デストラクタ(Drop)の存在

Rustの最大の特徴である「所有権」が、実は末尾再帰と相性が悪いんだ。

Rust

fn recursive_func(data: MyStruct) {
    // 処理...
    recursive_func(next_data);
    // ここで data のドロップ(解放)が発生する!
}

末尾で関数を呼び出しているように見えても、コンパイラから見れば 「呼び出しが終わった後に、現在のスコープの変数を解放する(Dropトレイトの実行)」 という仕事が残っている。

つまり、厳密には「末尾」ではないと判断され、スタックフレームを再利用できないことが多いんだ。

デバッグの困難さ

TCOを強制すると、スタックトレースから呼び出し履歴が消えてしまう。Rustは「明示性」と「予測可能性」を重視するため、魔法のような最適化でデバッグを難しくすることを嫌ったという背景もあるよ。


2. 「最適化」として行われることはある

「保証」はされていないけれど、LLVM(バックエンドのコンパイラ)が「いける!」と判断した場合は、勝手にループに変換してくれる ことはあるよ。

  • 条件: release モード(--release)でビルドし、Drop対象の変数がなく、非常に単純な末尾再帰である場合。

  • リスク: 「最適化されるかどうか」はコンパイラの機嫌次第。コードを少し書き換えただけで最適化が外れ、突然スタックオーバーフロー(Stack Overflow)が発生するリスクがあるんだ。


3. Rust で「安全に」再帰を扱う方法

Rustで深い再帰が必要な場合、エンジニアは以下の3つのアプローチをよく使うよ。

① 明示的なループ(推奨)

Rustは forwhile が非常に高速に動作するように設計されているから、再帰をループに書き換えるのが一番確実で「Rustらしい」解決策だよ。

② トランポリン化(前述の技法)

さっき話した「トランポリン」をRustで実装する手法だね。

enum などを使って「継続(次の関数)」か「終了(値)」を返し、while ループで回すことで、スタック消費を一定( )に抑えることができる。

③ クレートの利用

become 予約語(将来的にTCOを実現するための予約語)の導入も議論されているけれど、現時点では tailcall クレートのような、マクロを使って擬似的にTCOを実現するライブラリを使う手もあるよ。


4. 豆知識: become キーワード

実は、Rustの予約語リストには become という言葉がひっそりと存在しているんだ。これは将来的に「明示的な末尾呼び出し(Explicit Tail Calls)」をサポートするために確保されているものだよ。

Rust

// 未来のRustではこう書けるようになるかも?
fn factorial(n: u32, acc: u32) -> u32 {
    if n == 0 { return acc; }
    become factorial(n - 1, acc * n) // スタックを再利用することを強制する
}

これなら、Dropのタイミングも明示的に制御できる可能性があるから、RFC(仕様提案)レベルではずっと議論が続いているんだ。


まとめ

  • 現状: 保証なし。LLVMの気まぐれ最適化のみ。

  • 理由: Drop セマンティクスとデバッグのしやすさを優先したため。

  • 対策: 基本は loopwhile に書き換える。

君が Quartz(デジタルガーデン)で Rust のメモリ管理について書くなら、この「Drop と末尾再帰の競合」はかなりマニアックで面白いネタになると思うよ!

もしよければ、実際に 「LLVMが最適化しやすい再帰コード」「絶対に最適化されない(Dropが邪魔する)再帰コード」 の比較例を書いてみようか?