Kevin explique succinctement le fonctionnement de cet extrait de code (avec la raison pour laquelle il est tout à fait incompréhensible), mais je voulais ajouter quelques informations sur le fonctionnement des trampolines en général .
Sans optimisation d'appel final (TCO), chaque appel de fonction ajoute un cadre de pile à la pile d'exécution en cours. Supposons que nous ayons une fonction pour imprimer un compte à rebours de nombres:
function countdown(n) {
if (n === 0) {
console.log("Blastoff!");
} else {
console.log("Launch in " + n);
countdown(n - 1);
}
}
Si nous appelons countdown(3)
, analysons l’apparence de la pile d’appel sans le coût total de possession.
> 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
Avec le TCO, chaque appel récursif à countdown
est en queue (il ne reste plus qu'à renvoyer le résultat de l'appel), de sorte qu'aucune trame de pile n'est allouée. Sans TCO, la pile explose même légèrement n
.
Le trampoline contourne cette restriction en insérant un wrapper autour de la countdown
fonction. Ensuite, countdown
n'effectue pas d'appels récursifs mais renvoie immédiatement une fonction à appeler. Voici un exemple d'implémentation:
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);
}
}
Pour mieux comprendre comment cela fonctionne, regardons la pile d'appels:
> 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
A chaque étape , la countdownHop
fonction abandonne le contrôle direct de ce qui se passe ensuite, de retour à la place d' une fonction d'appel qui décrit ce qu'il vous se passer ensuite. La fonction de trampoline prend alors ceci et l'appelle, puis appelle la fonction qui revient, et ainsi de suite jusqu'à ce qu'il n'y ait plus d '"étape suivante". Cela s'appelle trampoline parce que le flux de contrôle "rebondit" entre chaque appel récursif et l'implémentation de trampoline, au lieu de la fonction directement récurrente. En abandonnant le contrôle sur qui fait l'appel récursif, la fonction de trampoline peut assurer la pile ne soit pas trop grand. Note latérale: cette implémentation trampoline
omet de renvoyer des valeurs pour plus de simplicité.
Il peut être difficile de savoir si c'est une bonne idée. Les performances peuvent pâtir de chaque étape consistant à allouer une nouvelle fermeture. Des optimisations intelligentes peuvent rendre cela viable, mais on ne sait jamais. Le trampoline est principalement utile pour contourner les limites strictes de la récursivité, par exemple lorsqu'une implémentation linguistique définit une taille de pile d'appel maximale.
loopy
ne déborde pas parce qu'il ne s'appelle pas lui-même .