Je fais du JavaScript fonctionnel. Je pensais que l’ optimisation des appels en attente avait été mise en œuvre, mais j’ai eu tort. Ainsi, j'ai dû apprendre moi-même le trampoline . Après un peu de lecture ici et ailleurs, j'ai pu comprendre les bases et construire mon premier trampoline:
/*not the fanciest, it's just meant to
reenforce that I know what I'm doing.*/
function loopy(x){
if (x<10000000){
return function(){
return loopy(x+1)
}
}else{
return x;
}
};
function trampoline(foo){
while(foo && typeof foo === 'function'){
foo = foo();
}
return foo;
/*I've seen trampolines without this,
mine wouldn't return anything unless
I had it though. Just goes to show I
only half know what I'm doing.*/
};
alert(trampoline(loopy(0)));
Mon plus gros problème est que je ne sais pas pourquoi cela fonctionne. J'ai eu l'idée de réexécuter la fonction dans une boucle while au lieu d'utiliser une boucle récursive. Sauf que, techniquement, ma fonction de base a déjà une boucle récursive. Je n'exécute pas la loopy
fonction de base , mais je l'exécute à l'intérieur. Qu'est-ce qui empêche foo = foo()
de provoquer un débordement de pile? Et n'est-ce pas foo = foo()
techniquement en mutation, ou est-ce que je manque quelque chose? Peut-être que c'est juste un mal nécessaire. Ou une syntaxe me manque.
Y a-t-il même un moyen de le comprendre? Ou est-ce juste un bidouillage qui fonctionne? J'ai pu parcourir tout le reste, mais celui-ci me laisse perplexe.
loopy
ne déborde pas parce qu'il ne s'appelle pas lui-même .Réponses:
La raison pour laquelle votre cerveau se rebelle contre cette fonction
loopy()
est qu’elle est de type incohérent :Un grand nombre de langues ne vous permettent même pas de faire des choses comme celle-ci, ou du moins exigent beaucoup plus de dactylographie pour expliquer à quel point cela est sensé. Parce que ce n'est vraiment pas le cas. Les fonctions et les entiers sont des types d'objets totalement différents.
Alors passons en revue cette boucle while:
Initialement,
foo
est égal àloopy(0)
. C'est quoiloopy(0)
? Eh bien, c'est moins de 10000000, alors nous avonsfunction(){return loopy(1)}
. C'est une valeur de vérité, et c'est une fonction, donc la boucle continue.Maintenant nous arrivons à
foo = foo()
.foo()
est le même queloopy(1)
. Puisque 1 est toujours inférieur à 10000000, cela retournefunction(){return loopy(2)}
, auquel nous attribuons ensuitefoo
.foo
est toujours une fonction, alors nous continuons ... jusqu'à ce que foo soit égal àfunction(){return loopy(10000000)}
. C'est une fonction, donc nous faisonsfoo = foo()
une fois de plus, mais cette fois, lorsque nous appelonsloopy(10000000)
, x n'est pas inférieur à 10000000, donc nous récupérons simplement x. Comme 10000000 n’est pas non plus une fonction, cela termine également la boucle while.la source
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:
Si nous appelons
countdown(3)
, analysons l’apparence de la pile d’appel sans le coût total de possession.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èrementn
.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:Pour mieux comprendre comment cela fonctionne, regardons la pile d'appels:
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émentationtrampoline
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.
la source
Il devient peut-être plus facile de comprendre si le trampoline est implémenté avec un type de retour dédié (au lieu d’abuser d’une fonction):
Comparez cela à votre version de
trampoline
, où le cas de récursivité est celui où la fonction renvoie une autre fonction et le cas de base, le cas échéant.Il ne s'appelle plus. Au lieu de cela, il retourne un résultat (dans mon implémentation, littéralement a
Result
) qui indique s'il faut continuer la récursion ou s'ouvrir.Oui, c'est exactement le mal nécessaire de la boucle. On pourrait aussi écrire
trampoline
sans mutation, mais cela nécessiterait à nouveau une récursion:Néanmoins, cela montre encore mieux l’utilité de la fonction de trampoline.
Le point de trampoline consiste à extraire l'appel récursif de la fonction qui veut utiliser la récursion dans une valeur de retour et à faire la récursion réelle à un seul endroit - la
trampoline
fonction peut ensuite être optimisée à un seul endroit pour utiliser un boucle.la source
foo = foo()
C'est une mutation dans le sens de la modification de l'état local, mais je considérerais généralement cette réaffectation car vous ne modifiez pas réellement l'objet de fonction sous-jacent, vous le remplacez par la fonction (ou la valeur) qu'il renvoie.foo
contient, seule la variable est modifiée. Unewhile
boucle nécessite un état modifiable si vous voulez qu'elle se termine, dans ce cas la variablefoo
oux
.fn
en un appel récursif àtrampoline
- je ne suis pas sûr que ce soit une amélioration.