Voici le code:
function repeat(operation, num) {
return function() {
if (num <= 0) return
operation()
return repeat(operation, --num)
}
}
function trampoline(fn) {
while(fn && typeof fn === 'function') {
fn = fn()
}
}
module.exports = function(operation, num) {
trampoline(function() {
return repeat(operation, num)
})
}
J'ai lu que le trampoline est utilisé pour traiter les problèmes de débordement, de sorte que la fonction ne se contenterait pas d'appeler elle-même et d'empiler la pile.
Mais comment fonctionne cet extrait de code? Surtout la fonction trampoline
? Qu'a-t-il fait exactement par while
et comment at-il atteint son objectif?
Merci pour toute aide :)
La boucle while
continuera de fonctionner jusqu'à ce que la condition soit fausse.
fn && typeof fn === 'function'
sera falsifié si fn
lui-même est faux, ou si fn
est autre chose qu'une fonction.
La première moitié est en fait redondante, car les valeurs de falsification ne sont pas non plus des fonctions.
Le trampoline est juste une technique pour optimiser la récursivité et empêcher les exceptions de débordement de pile dans les langues qui ne prennent pas en charge tail call optimization
comme l'implémentation Javascript ES5 et C #. Cependant, ES6 prendra probablement en charge l'optimisation des appels de queue.
Le problème avec la récursivité régulière est que chaque appel récursif ajoute un cadre de pile à la pile d'appels, que vous pouvez visualiser comme une pyramide d'appels. Voici une visualisation de l'appel récursif d'une fonction factorielle:
(factorial 3)
(* 3 (factorial 2))
(* 3 (* 2 (factorial 1)))
(* 3 (* 2 (* 1 (factorial 0))))
(* 3 (* 2 (* 1 1)))
(* 3 (* 2 1))
(* 3 2)
6
Voici une visualisation de la pile où chaque tiret vertical est un cadre de pile:
---|---
---| |---
---| |---
--- ---
Le problème est que la pile a une taille limitée et l'empilement de ces cadres de pile peut déborder la pile. Selon la taille de la pile, un calcul d'une factorielle plus grande déborderait la pile. C'est pourquoi une récursivité régulière en C #, Javascript etc. pourrait être considérée dangereuse .
Un modèle d'exécution optimal serait quelque chose comme un trampoline au lieu d'une pyramide, où chaque appel récursif est exécuté sur place et ne se cumule pas sur l'appel empiler. Cette exécution dans des langages prenant en charge l'optimisation des appels de queue pourrait ressembler à:
(fact 3)
(fact-tail 3 1)
(fact-tail 2 3)
(fact-tail 1 6)
(fact-tail 0 6)
6
Vous pouvez visualiser la pile comme un trampoline rebondissant:
---|--- ---|--- ---|---
--- --- ---
C'est nettement mieux car la pile n'a toujours qu'un seul cadre, et à partir de la visualisation, vous pouvez également voir pourquoi on l'appelle un trampoline. Cela empêche la pile de déborder.
Puisque nous n'avons pas le luxe de tail call optimization
en Javascript, nous devons trouver un moyen de transformer la récursivité régulière en une version optimisée qui s'exécutera à la manière du trampoline.
Une façon évidente est de se débarrasser de la récursivité et de réécrire le code pour être itératif.
Lorsque cela n'est pas possible, nous avons besoin d'un code un peu plus complexe où, au lieu d'exécuter directement les étapes récursives, nous utiliserons higher order functions
pour renvoyer une fonction wrapper au lieu d'exécuter directement l'étape récursive et laisser une autre fonction contrôler l'exécution.
Dans votre exemple, la fonction repeat encapsule l'appel récursif normal avec une fonction, et elle renvoie cette fonction au lieu d'exécuter l'appel récursif:
function repeat(operation, num) {
return function() {
if (num <= 0) return
operation()
return repeat(operation, --num)
}
}
La fonction retournée est la prochaine étape de l'exécution récursive et le trampoline est un mécanisme pour exécuter ces étapes de manière contrôlée et itérative dans la boucle while:
function trampoline(fn) {
while(fn && typeof fn === 'function') {
fn = fn()
}
}
Ainsi, le seul but de la fonction trampoline est de contrôler l'exécution de manière itérative, et cela garantit que la pile ne dispose que d'un seul cadre de pile sur la pile à un moment donné.
L'utilisation d'un trampoline est évidemment moins performante que la simple récursivité, puisque vous "bloquez" le flux récursif normal, mais c'est beaucoup plus sûr.
Les autres réponses décrivent le fonctionnement d'un trampoline. La mise en œuvre donnée présente cependant deux inconvénients, dont l'un est même nocif:
Essentiellement, la technique du trampoline traite de l'évaluation paresseuse dans un langage évalué avec impatience. Voici une approche qui évite les inconvénients mentionnés ci-dessus:
// a tag to uniquely identify thunks (zero-argument functions)
const $thunk = Symbol.for("thunk");
// eagerly evaluate a lazy function until the final result
const eager = f => (...args) => {
let g = f(...args);
while (g && g[$thunk]) g = g();
return g;
};
// lift a normal binary function into the lazy context
const lazy2 = f => (x, y) => {
const thunk = () => f(x, y);
return (thunk[$thunk] = true, thunk);
};
// the stack-safe iterative function in recursive style
const repeat = n => f => x => {
const aux = lazy2((n, x) => n === 0 ? x : aux(n - 1, f(x)));
return eager(aux) (n, x);
};
const inc = x => x + 1;
// and run...
console.log(repeat(1e6) (inc) (0)); // 1000000
L'évaluation paresseuse a lieu localement à l'intérieur de repeat
. Par conséquent, votre code d'appel n'a pas à s'en soucier.