web-dev-qa-db-fra.com

Comment comprendre le trampoline en JavaScript?

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 :)

45
Zhen Zhang

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.

7
SLaks

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.

http://en.wikipedia.org/wiki/Tail_call

http://en.wikipedia.org/wiki/Trampoline_%28computing%29

126
Faris Zacina

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:

  • Le protocole du trampoline ne dépend que des fonctions. Et si le résultat de l'opération récursive est également une fonction?
  • Vous devez appliquer la fonction récursive avec la fonction trampoline tout au long de votre code d'appel. Il s'agit d'un détail d'implémentation qui doit être masqué.

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.

3
user6445533