web-dev-qa-db-ja.com

JavaScriptでトランポリンを理解する方法は?

これがコードです:

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

私は、トランポリンがオーバーフローの問題を処理するために使用されるので、関数が呼び出し自体とスタックを保持し続けるだけではないことを読みました。

しかし、このスニペットはどのように機能しますか?特にtrampoline関数は?それはwhileによって正確に何をしましたか、そしてどのようにしてその目的を達成しましたか?

助けてくれてありがとう:)

45
Zhen Zhang

whileループは、条件が偽になるまで実行を続けます。

fn && typeof fn === 'function'は、fn自体が偽である場合、またはfnが関数以外の場合である場合、偽になります。

偽の値も機能しないため、前半は実際には冗長です。

7
SLaks

トランポリンは、recursionを最適化し、tail call optimizationのようなJavascript ES5実装やC#。ただし、ES6はおそらく末尾呼び出しの最適化をサポートします。

通常の再帰の問題は、すべての再帰呼び出しがスタックフレームを呼び出しスタックに追加することです。これは、呼び出しのpyramidとして視覚化できます。これは、階乗関数を再帰的に呼び出す視覚化です:

(factorial 3)
(* 3 (factorial 2))
(* 3 (* 2 (factorial 1)))
(* 3 (* 2 (* 1 (factorial 0)))) 
(* 3 (* 2 (* 1 1)))
(* 3 (* 2 1))
(* 3 2)
6

以下は、各垂直ダッシュがスタックフレームであるスタックの視覚化です。

         ---|---
      ---|     |---
   ---|            |--- 
---                    ---

問題は、スタックのサイズに制限があり、これらのスタックフレームをスタックするとスタックがオーバーフローする可能性があることです。スタックサイズに応じて、より大きい階乗の計算はスタックをオーバーフローします。これが、C#、Javascriptなどの定期的な再帰がdangerousと見なされる理由です。

最適な実行モデルは、ピラミッドの代わりにtrampolineのようなものであり、各再帰呼び出しはその場で実行され、呼び出しにスタックしません。スタック。末尾呼び出しの最適化をサポートする言語での実行は次のようになります。

(fact 3)
(fact-tail 3 1)
(fact-tail 2 3)
(fact-tail 1 6)
(fact-tail 0 6)
6

跳ねるトランポリンのようにスタックを視覚化できます。

   ---|---   ---|---   ---|---
---      ---       ---       

スタックには常に1つのフレームしかないため、これは明らかに優れています。視覚化から、トランポリンと呼ばれる理由もわかります。これにより、スタックがオーバーフローするのを防ぎます。

JavaScriptにはtail call optimizationの贅沢がないため、定期的な再帰をトランポリン形式で実行する最適化されたバージョンに変換する方法を見つける必要があります。

明らかな方法の1つは、再帰を取り除き、コードを反復的に書き換えることです。

それが不可能な場合は、再帰ステップを直接実行する代わりに、higher order functionsを使用して、再帰ステップを直接実行する代わりにラッパー関数を返し、別の関数に実行を制御させる、もう少し複雑なコードが必要です。

あなたの例では、repeat関数は通常の再帰呼び出しを関数でラップし、再帰呼び出しを実行する代わりにその関数を返します:

function repeat(operation, num) {
    return function() {
       if (num <= 0) return
       operation()
       return repeat(operation, --num)
    }
}

返された関数は再帰実行の次のステップであり、トランポリンは、whileループでこれらのステップを制御された反復的な方法で実行するメカニズムです。

function trampoline(fn) {
    while(fn && typeof fn === 'function') {
        fn = fn()
    }
}

したがって、トランポリン関数の唯一の目的は、反復的な方法で実行を制御することであり、これにより、スタックは常に、スタック上に単一のスタックフレームのみを持つようになります。

通常の再帰フローを「ブロック」しているため、トランポリンを使用すると、単純な再帰よりも明らかにパフォーマンスが低下しますが、はるかに安全です。

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

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

126
Faris Zacina

他の返信はトランポリンがどのように機能するかを説明しています。ただし、指定された実装には2つの欠点があり、そのうちの1つは有害です。

  • トランポリンプロトコルは機能のみに依存します。再帰演算の結果も関数である場合はどうなりますか?
  • 呼び出しコード全体で、トランポリン関数を使用して再帰関数を適用する必要があります。これは、非表示にする必要がある実装の詳細です。

基本的に、トランポリン手法は、熱心に評価される言語での遅延評価を扱います。上記の欠点を回避する方法を次に示します。

// 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

遅延評価はrepeat内でローカルに行われます。したがって、呼び出し元のコードはそれを心配する必要はありません。

3
user6445533