凯文简洁地指出了这个特定的代码片段是如何工作的(以及为什么它很难理解),但是我想补充一些有关蹦床在一般情况下如何工作的信息。
如果没有尾部调用优化(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
。
Trampolining通过在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
为简化起见,此实现省略了返回值。
知道这是否是一个好主意可能很棘手。由于分配新封包的每个步骤都会影响性能。明智的优化可以使之可行,但您永远不会知道。Trampolining对于绕开硬递归限制非常有用,例如,当语言实现设置最大调用堆栈大小时。
loopy
不会溢出,因为它不会自己调用。