【まつびよびだしさいてきか】

末尾呼び出し最適化 とは?

💡 再帰のたびに積み上がる「お皿の山」を、1枚だけ使い回す賢い節約術
📌 このページのポイント
末尾呼び出し最適化(TCO): スタックの比較 TCOなし(通常の再帰) fact(5) — 待機中 fact(4) — 待機中 fact(3) — 待機中 fact(2) — 待機中 fact(1) — 実行中 5枚 積み上がる N回 → N個のスタック消費 大量再帰でオーバーフロー! TCOあり(末尾再帰) fact(5,1) — 上書き済 fact(4,5) — 上書き済 fact(3,20) — 上書き済 fact(2,60) — 上書き済 fact(1,120) — 実行中 1枚を 使い回し N回 → 常に1個のスタック ループと同じ効率!
TCOなし(スタック積み上がり)とTCOあり(スタック再利用)の比較
ひよこ ひよこ

末尾呼び出し最適化って何?名前が長くて難しそう…

ペンギン先生 ペンギン先生

略してTCOとも呼ばれるよ。まず「末尾呼び出し」から説明すると、関数の一番最後の処理が「別の関数を呼ぶだけ」のパターンのことだよ。この場合、呼び出し元の情報をメモリに残しておく必要がないから、スタックフレームを使い回せるんだ。

ひよこ ひよこ

スタックフレームって何なの?

ペンギン先生 ペンギン先生

関数を呼ぶたびに、戻り先やローカル変数の情報がメモリに積み上がるんだ。これがスタックフレーム。お皿を重ねるイメージだね。再帰で1万回関数を呼ぶと1万枚のお皿が積まれて、限界を超えるとスタックオーバーフローでクラッシュしてしまうんだよ。

ひよこ ひよこ

TCOがあるとお皿が積み上がらないの?

ペンギン先生 ペンギン先生

そうだよ!末尾呼び出しの場合、呼び出し元にもう戻る必要がないから、お皿を新しく積む代わりに今のお皿を上書きできるんだ。だから再帰を100万回やっても、メモリ的にはお皿1枚分で済む。ループと同じ効率で再帰が書けるんだよ。

ひよこ ひよこ

どの言語でも使えるの?

ペンギン先生 ペンギン先生

言語やランタイムによるんだ。Scheme(Lispの方言)は言語仕様でTCOを保証していて、Haskell遅延評価と組み合わせてサポートしているよ。Scalaは @tailrec アノテーションで末尾再帰を強制チェックできる。JavaScriptはES2015仕様に入ったけど、実際に実装しているのは2026年時点でもSafariだけなんだ。

ひよこ ひよこ

普通の再帰と末尾再帰ってどう書き分けるの?

ペンギン先生 ペンギン先生

ポイントは「再帰呼び出しの結果に何もしない」こと。たとえば factorial(n) で return n * factorial(n-1) と書くと、再帰の結果に掛け算が必要だから末尾呼び出しにならない。代わりにアキュムレータ引数を使って return factorial(n-1, acc*n) と書けば、最後の処理が関数呼び出しだけになって末尾再帰になるよ。この書き換えパターンを覚えておくと、関数型プログラミング再帰を安全に使いこなせるようになるね。

ペンギン
まとめ:ざっくりこれだけ覚えればOK!
「末尾呼び出し最適化」って出てきたら「再帰スタックが溢れないようにする最適化」と思えればだいたいOK!
📖 おまけ:英語の意味
「Tail Call Optimization」 = 末尾呼び出しの最適化
💬 Tail(末尾)の関数Call(呼び出し)をOptimize(最適化)するという、そのままの名前だよ
← 用語集にもどる