Kevinは、この特定のコードスニペットがどのように機能するかを(簡潔に理解できない理由も含めて)簡潔に指摘していますが、一般的なトランポリンの動作に関する情報を追加したいと思いました。
末尾呼び出し最適化(TCO)を使用しない場合、すべての関数呼び出しは現在の実行スタックにスタックフレームを追加します。数値のカウントダウンを出力する関数があるとします:
function countdown(n) {
if (n === 0) {
console.log("Blastoff!");
} else {
console.log("Launch in " + n);
countdown(n - 1);
}
}
を呼び出す場合countdown(3)
、TCOなしでコールスタックがどのように見えるかを分析しましょう。
> countdown(3);
// stack: countdown(3)
Launch in 3
// stack: countdown(3), countdown(2)
Launch in 2
// stack: countdown(3), countdown(2), countdown(1)
Launch in 1
// stack: countdown(3), countdown(2), countdown(1), countdown(0)
Blastoff!
// returns, stack: countdown(3), countdown(2), countdown(1)
// returns, stack: countdown(3), countdown(2)
// returns, stack: countdown(3)
// returns, stack is empty
TCOでは、への各再帰呼び出しcountdown
は末尾位置にあるため(呼び出しの結果を返す以外に何もする必要はありません)、スタックフレームは割り当てられません。TCOがなければ、スタックはわずかに大きくなりn
ます。
トランポリンは、countdown
関数の周りにラッパーを挿入することにより、この制限を回避します。次に、countdown
再帰呼び出しを実行せず、代わりに呼び出す関数をすぐに返します。以下に実装例を示します。
function trampoline(firstHop) {
nextHop = firstHop();
while (nextHop) {
nextHop = nextHop()
}
}
function countdown(n) {
trampoline(() => countdownHop(n));
}
function countdownHop(n) {
if (n === 0) {
console.log("Blastoff!");
} else {
console.log("Launch in " + n);
return () => countdownHop(n-1);
}
}
これがどのように機能するかをよりよく理解するために、コールスタックを見てみましょう。
> countdown(3);
// stack: countdown(3)
// stack: countdown(3), trampoline
// stack: countdown(3), trampoline, countdownHop(3)
Launch in 3
// return next hop from countdownHop(3)
// stack: countdown(3), trampoline
// trampoline sees hop returned another hop function, calls it
// stack: countdown(3), trampoline, countdownHop(2)
Launch in 2
// stack: countdown(3), trampoline
// stack: countdown(3), trampoline, countdownHop(1)
Launch in 1
// stack: countdown(3), trampoline
// stack: countdown(3), trampoline, countdownHop(0)
Blastoff!
// stack: countdown(3), trampoline
// stack: countdown(3)
// stack is empty
各ステップで、countdownHop
機能を放棄する代わりに、それを呼び出すための関数を返す、次に何が起こるかを直接制御することがどうなるかを説明したいと次に何が起こるします。トランポリン関数は、これを取得して呼び出し、次に返される関数を呼び出し、「次のステップ」がなくなるまで続けます。これは、関数の直接的な繰り返しではなく、制御のフローが各再帰呼び出しとトランポリン実装の間で「バウンス」するため、トランポリンと呼ばれます。誰が再帰呼び出しを行うかの制御を放棄することにより、トランポリン関数はスタックが大きくなりすぎないようにすることができます。補足:この実装でtrampoline
は、簡単にするために値を返すことを省略しています。
これが良いアイデアかどうかを知るのは難しいかもしれません。各ステップが新しいクロージャを割り当てるため、パフォーマンスが低下する可能性があります。巧妙な最適化はこれを実行可能にしますが、あなたは決して知りません。トランポリンは、言語の実装が最大呼び出しスタックサイズを設定する場合など、ハード再帰の制限を回避するために最も役立ちます。
loopy
それはそれ自身を呼び出さないので、オーバーフローしません。