これは、再帰プロシージャを末尾再帰に変換する一般的な方法ですか?


13

私が変換する一般的な方法見つけたようだ任意の末尾再帰に再帰的な手順を:

  1. 追加の「結果」パラメーターを使用して、ヘルパーサブプロシージャを定義します。
  2. プロシージャの戻り値に適用されるものをそのパラメータに適用します。
  3. このヘルパープロシージャを呼び出して開始します。「結果」パラメーターの初期値は、再帰プロセスの終了点の値であるため、結果の反復プロセスは、再帰プロセスが縮小し始めるところから始まります。

たとえば、変換する元の再帰的手順は次のとおりです(SICP演習1.17)。

(define (fast-multiply a b)
  (define (double num)
    (* num 2))
  (define (half num)
    (/ num 2))
  (cond ((= b 0) 0)
        ((even? b) (double (fast-multiply a (half b))))
        (else (+ (fast-multiply a (- b 1)) a))))

変換された末尾再帰手続き(SICP演習1.18)は次のとおりです。

(define (fast-multiply a b)
  (define (double n)
    (* n 2))
  (define (half n)
    (/ n 2))
  (define (multi-iter a b product)
    (cond ((= b 0) product)
          ((even? b) (multi-iter a (half b) (double product)))
          (else (multi-iter a (- b 1) (+ product a)))))
  (multi-iter a b 0))

誰かがこれを証明または反証できますか?


1
Oログn

2番目の考え:b2のべき乗を選択すると、最初はproduct0に設定するのは適切ではないことがわかります。しかし、1に変更しても、b奇数の場合は機能しません。たぶん、2つの異なるアキュムレータパラメータが必要ですか?
j_random_hacker

3
実際には、非末尾再帰定義の変換を定義しておらず、結果パラメーターを追加して累積に使用することはかなりあいまいであり、2つの再帰呼び出しがあるツリートラバーサルなど、より複雑なケースに一般化することはほとんどありません。ただし、「継続」のより正確なアイデアが存在します。そこでは、作業の一部を行い、「継続」機能を引き継いで、これまでに行った作業をパラメーターとして受け取ります。継続渡しスタイル(cps)と呼ばれます。en.wikipedia.org/ wiki / Continuation-passing_styleを参照してください。
アリエル

4
これらのスライドfsl.cs.illinois.edu/images/d/d5/CS422-Fall-2006-13.pdfには、cps変換の説明が含まれています。この変換では、任意の式を使用します(非テールコールの関数定義を使用する場合があります)そして、それを末尾呼び出しのみを持つ同等の式に変換します。
アリエル

@j_random_hackerはい、「変換された」手順が実際に間違っていることが
わかり

回答:


12

アルゴリズムの説明はあまりにも曖昧なので、この時点では評価できません。しかし、ここで考慮すべきことがいくつかあります。

CPS

実際、変換する方法があります 任意の唯一の末尾呼び出しを使用して、フォームにコードを。これはCPS変換です。CPS(Continuation-Passing Style)は、各関数に継続を渡すことでコードを表現する形式です。継続とは、「計算の残り」を表す抽象的な概念です。CPS形式で表現されたコードでは、継続を具体化する自然な方法は、値を受け入れる関数としてです。CPSでは、値を返す関数の代わりに、現在の継続を表す関数を、関数によって「返される」ことに適用します。

たとえば、次の関数を考えます。

(lambda (a b c d)
  (+ (- a b) (* c d)))

これは、次のようにCPSで表現できます。

(lambda (k a b c d)
  (- (lambda (v1)
       (* (lambda (v2)
            (+ k v1 v2))
          a b))
     c d))

ugくて、しばしば遅いですが、確かな利点があります:

  • 変換は完全に自動化できます。したがって、コードをCPS形式で記述する(または表示する)必要はありません。
  • と組み合わせ サンクおよびトランポリンと、テールコールの最適化を提供しない言語でテールコールの最適化を提供するために使用できます。(直接末尾再帰関数の末尾呼び出し最適化は、再帰呼び出しをループに変換するなど、他の手段で実現できます。ただし、間接再帰はこの方法で変換するのは簡単ではありません。)
  • CPSでは、継続が第一級のオブジェクトになります。継続は制御の本質であるため、これにより、言語からの特別なサポートを必要とせずに、実質的にすべての制御オペレーターをライブラリとして実装できます。たとえば、goto、例外、および協調スレッドはすべて、継続を使用してモデル化できます。

TCO

テール再帰(またはテールコール全般)に関係する唯一の理由は、テールコール最適化(TCO)のためであるように思えます。したがって、私が尋ねるべきより良い質問は、「私の変換は末尾呼び出しが最適化可能なコードを生成するか?」だと思います。

もう一度CPSを検討すると、その特徴の1つは、CPSで表現されたコードがテールコールのみで構成されることです。すべてが末尾呼び出しであるため、スタックに戻り点を保存する必要はありません。したがって、CPS形式のすべてのコードは、末尾呼び出しに最適化する必要ありますよね?

まあ、そうではありません。スタックを削除したように見えるかもしれませんが、それを表現する方法を変更するだけです。スタックは、継続を表すクロージャーの一部になりました。そのため、CPSはすべてのコードの末尾呼び出しを魔法のように最適化するわけではありません。

CPSですべてをTCOにできない場合、直接再帰専用のトランスフォームはありますか?いいえ、一般的ではありません。いくつかの再帰は線形ですが、いくつかはそうではありません。非線形(ツリーなど)の再帰は、単にどこかで可変量の状態を維持する必要があります。


TCO」サブセクションで「テールコール最適化」と言うと、実際には「一定のメモリ使用量」を意味する場合、少し混乱します。動的なメモリ使用量が一定ではないということは、呼び出しが実際にテールであり、スタック使用量に限りない増加がないという事実を否定しません。SICPはそのような計算を「反復的」と呼んでいるので、「TCOであるにもかかわらず、反復的ではない」と言う方が良い表現であったかもしれません(私にとって)。
ウィルネス

@WillNess呼び出しスタックはまだありますが、異なる表現になっています。ハードウェアスタックではなく、ヒープを使用しているからといって、構造は変わりません。結局のところ、名前に「スタック」が含まれる動的なヒープメモリに基づくデータ構造がたくさんあります。
ネイサンデイビス

ここでの唯一のポイントは、一部の言語にはコールスタックの使用に固定された制限があることです。
ウィルネス
弊社のサイトを使用することにより、あなたは弊社のクッキーポリシーおよびプライバシーポリシーを読み、理解したものとみなされます。
Licensed under cc by-sa 3.0 with attribution required.