Kevin sottolinea brevemente come funziona questo particolare frammento di codice (insieme al motivo per cui è abbastanza incomprensibile), ma volevo aggiungere alcune informazioni su come funzionano i trampolini in generale .
Senza l'ottimizzazione del tail-call (TCO), ogni chiamata di funzione aggiunge un frame di stack allo stack di esecuzione corrente. Supponiamo di avere una funzione per stampare un conto alla rovescia di numeri:
function countdown(n) {
if (n === 0) {
console.log("Blastoff!");
} else {
console.log("Launch in " + n);
countdown(n - 1);
}
}
Se chiamiamo countdown(3)
, analizziamo come apparirebbe lo stack di chiamate senza 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
Con il TCO, ogni chiamata ricorsiva verso countdown
è nella posizione di coda (non c'è altro da fare che restituire il risultato della chiamata), quindi non viene assegnato alcun frame di stack. Senza TCO, lo stack esplode per anche leggermente più grande n
.
Il trampolino elimina questa limitazione inserendo un wrapper attorno alla countdown
funzione. Quindi, countdown
non esegue chiamate ricorsive e restituisce immediatamente una funzione da chiamare. Ecco un esempio di implementazione:
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);
}
}
Per capire meglio come funziona, diamo un'occhiata allo stack di chiamate:
> 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
Ad ogni passo la countdownHop
funzione abbandona controllo diretto di ciò che accade dopo, invece tornando una funzione per chiamare che descrive quello che sarebbe desidera per accadere. La funzione trampolino quindi prende questo e lo chiama, quindi chiama qualsiasi funzione che ritorna e così via fino a quando non vi è alcun "passaggio successivo". Questo si chiama trampolino in quanto il flusso di controllo "rimbalza" tra ogni chiamata ricorsiva e l'implementazione del trampolino, invece della funzione che ricorre direttamente. Abbandonando il controllo su chi effettua la chiamata ricorsiva, la funzione trampolino può garantire che lo stack non diventi troppo grande. Nota a margine: questa implementazione di trampoline
omette la restituzione di valori per semplicità.
Può essere difficile sapere se questa è una buona idea. Le prestazioni possono risentire di ogni passaggio che assegna una nuova chiusura. Ottimizzazioni intelligenti possono renderlo praticabile, ma non lo sai mai. Il trampolino è utile soprattutto per aggirare i limiti di ricorsione, ad esempio quando un'implementazione del linguaggio imposta una dimensione massima dello stack di chiamate.
loopy
non trabocca perché non si chiama da solo .