Kevin aponta sucintamente como esse trecho de código específico funciona (junto com o motivo pelo qual é bastante incompreensível), mas eu queria adicionar algumas informações sobre como os trampolins em geral funcionam.
Sem otimização de chamada de cauda (TCO), toda chamada de função adiciona um quadro de pilha à pilha de execução atual. Suponha que tenhamos uma função para imprimir uma contagem regressiva de números:
function countdown(n) {
if (n === 0) {
console.log("Blastoff!");
} else {
console.log("Launch in " + n);
countdown(n - 1);
}
}
Se ligarmos countdown(3)
, vamos analisar como ficaria a pilha de chamadas sem o 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
Com o TCO, cada chamada recursiva para countdown
está na posição final (não há mais nada a fazer além de retornar o resultado da chamada), portanto, nenhum quadro de pilha é alocado. Sem o TCO, a pilha explode para um pouco maior n
.
O trampolim contorna essa restrição inserindo um invólucro em torno da countdown
função. Em seguida, countdown
não realiza chamadas recursivas e retorna imediatamente uma função para chamar. Aqui está um exemplo de implementação:
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);
}
}
Para ter uma noção melhor de como isso funciona, vejamos a pilha de chamadas:
> 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
Em cada etapa, a countdownHop
função abandona o controle direto do que acontece a seguir, em vez disso, retorna uma função para chamar que descreve o que gostaria que acontecesse a seguir. A função trampolim então pega e chama, então chama qualquer função que retorna, e assim por diante até que não haja "próximo passo". Isso é chamado de trampolim porque o fluxo de controle "salta" entre cada chamada recursiva e a implementação do trampolim, em vez da função diretamente recorrente. Ao abandonar o controle sobre quem faz a chamada recursiva, a função trampolim pode garantir que a pilha não fique muito grande. Nota lateral: esta implementação trampoline
omite os valores retornados por simplicidade.
Pode ser complicado saber se essa é uma boa ideia. O desempenho pode sofrer devido a cada etapa que aloca um novo fechamento. Otimizações inteligentes podem tornar isso viável, mas você nunca sabe. O trampolim é útil principalmente para contornar limites rígidos de recursão, por exemplo, quando uma implementação de idioma define um tamanho máximo de pilha de chamadas.
loopy
não transborda porque não se chama .