web-dev-qa-db-ja.com

再帰では関数型言語の方が優れていますか?

TL; DR:関数型言語は、非関数型言語よりも再帰をうまく処理しますか?

私は現在、コードコンプリート2を読んでいます。本のある時点で、著者は再帰について警告しています。彼はそれは可能な限り避けられるべきであり、再帰を使用する関数は一般にループを使用する解決策よりも効果的でないと言います。例として、著者はJava関数を再帰を使用して次のように数値の階乗を計算することで作成しました(現時点では本を持っていないため、正確に同じではない可能性があります):

public int factorial(int x) {
    if (x <= 0)
        return 1;
    else
        return x * factorial(x - 1);
}

これは悪い解決策として提示されています。ただし、関数型言語では、多くの場合、再帰を使用することが望ましい方法です。たとえば、以下は再帰を使用したHaskellの階乗関数です。

factorial :: Integer -> Integer
factorial 0 = 1
factorial n = n * factorial (n - 1)

そして、良い解決策として広く受け入れられています。私が見てきたように、Haskellは再帰を頻繁に使用しており、眉をひそめている箇所はどこにも見当たりませんでした。

だから私の質問は基本的に:

  • 関数型言語は、非関数型言語よりも再帰をうまく処理しますか?

編集:私が使用した例は私の質問を説明するのに最適ではないことを認識しています。 Haskell(および一般的な関数型言語)が非関数型言語よりも再帰を使用する頻度が高いことを指摘したかっただけです。

42
marco-fiset

はい、そうですが、それはcanであるだけでなく、-する必要があるでもあるからです。

ここでの重要な概念は純度です。純粋な関数は、副作用も状態もない関数です。関数型プログラミング言語は一般に、コードについての推論や自明ではない依存関係の回避など、多くの理由から純粋性を受け入れます。一部の言語、特にHaskellはonlyの純粋なコードを許可することさえしています。プログラムが持つ可能性のある副作用(I/Oの実行など)は、純粋でないランタイムに移動され、言語自体が純粋に保たれます。

副作用がないことは、ループカウンターを持つことができないことを意味します(ループカウンターは変更可能な状態を構成し、そのような状態を変更することは副作用になるため)。したがって、純粋な関数型言語が得ることができる最も反復的なことは、 list(この操作は通常foreachまたはmapと呼ばれます)。ただし、再帰は純粋な関数型プログラミングと自然に一致します。(読み取り専用)関数の引数と(書き込み専用)の戻り値を除いて、再帰するための状態は必要ありません。

ただし、副作用がないことは、再帰をより効率的に実装できることを意味し、コンパイラーは再帰をより積極的に最適化できます。私はそのようなコンパイラーについて自分自身で詳しく調べたことはありませんが、私が知る限り、ほとんどの関数型プログラミング言語のコンパイラーは末尾呼び出しの最適化を実行します。

37
tdammers

再帰と反復を比較しています。 tail-call elimination を使用しない場合、余分な関数呼び出しがないため、反復は確かにより効率的です。また、反復は永遠に続く可能性がありますが、あまりにも多くの関数呼び出しからスタック領域が不足する可能性があります。

ただし、反復にはカウンターの変更が必要です。つまり、純粋に機能的な設定では禁止されている 可変変数 が必要です。そのため、関数型言語は反復を必要とせずに動作するように特別に設計されているため、関数呼び出しが合理化されています。

しかし、これらのいずれも、コードサンプルがそれほど洗練されていない理由に対処していません。あなたの例は、異なるプロパティ、つまり パターンマッチング を示しています。そのため、Haskellサンプルには明示的な条件がありません。つまり、コードを小さくするのは合理化された再帰ではありません。それはパターンマッチングです。

18
chrisaycock

技術的にはありませんが、実際にはあります。

問題に対して機能的なアプローチをとっている場合、再帰ははるかに一般的です。そのため、関数型のアプローチを使用するように設計された言語には、再帰をより簡単、より良い、より問題の少ないものにする機能が含まれていることがよくあります。私の頭の上には、3つの一般的なものがあります。

  1. テールコールの最適化。他のポスターで指摘されているように、関数型言語ではTCOが必要になることがよくあります。

  2. 遅延評価。Haskell(および他のいくつかの言語)は遅延評価されます。これにより、必要になるまでメソッドの実際の「作業」が遅延します。これは、より再帰的なデータ構造につながり、ひいては再帰的な方法でデータ構造を処理する傾向があります。

  3. 不変性。関数型プログラミング言語で扱うものの大部分は不変です。これにより、時間の経過に伴うオブジェクトの状態を気にする必要がないため、再帰が容易になります。たとえば、あなたの下から値を変更することはできません。また、多くの言語は 純粋な関数 を検出するように設計されています。純粋な関数には副作用がないため、コンパイラーは、関数の実行順序やその他の最適化について、より多くの自由度を持っています。

これらのものは、関数型言語に固有のものではなく、関数型言語に固有であるため、関数型であるため、単に優れているわけではありません。しかし、これらは機能的であるため、機能的なプログラミングを行う場合に有用である(そしてその欠点が問題にならない)ため、行われる設計決定はこれらの機能に向かう傾向があります。

5
Telastyn

私が知っている唯一の技術的な理由は、一部の関数型言語(および覚えている場合は一部の命令型言語)にいわゆる 末尾呼び出しの最適化 があることです。呼び出し(すなわち、再帰呼び出しは多かれ少なかれスタック上の現在の呼び出しを置き換えます)。

この最適化は、any再帰呼び出しでは機能せず、末尾呼び出し再帰メソッド(つまり、状態を維持しないメソッド)でのみ機能することに注意してください。再帰呼び出しの時間)

1
Steven Evers

Haskellおよびその他の関数型言語は、通常、遅延評価を使用します。この機能を使用すると、終了しない再帰関数を作成できます。

再帰が終了する基本ケースを定義せずに再帰関数を作成すると、その関数とstackoverflowが無限に呼び出されることになります。

Haskellは再帰的な関数呼び出しの最適化もサポートしています。 Javaでは、各関数呼び出しがスタックしてオーバーヘッドが発生します。

つまり、関数型言語は他の言語よりも再帰をうまく処理します。

1
Mert Akcakaya

ガベージコレクションは高速ですが、スタックは高速です 。Cプログラマーが「ヒープ」と考えるものの使用に関する論文です。コンパイルされたCのスタックフレームについて。筆者は、Gccをいじくり回してそうしたと思います。それは明確な答えではありませんが、再帰に関するいくつかの問題を理解するのに役立つかもしれません。

Bell LabsのPlan 9とともに使用されていた Alefプログラミング言語 には、「become」ステートメントがありました( this reference )。これは、一種の明示的な末尾呼び出しの再帰最適化です。 「しかし、それはコールスタックを使い果たします!」再帰に反対する議論は、おそらくなくなる可能性があります。

1
Bruce Ediger

TL; DR:はい、そうです
再帰は関数型プログラミングの重要なツールであるため、これらの呼び出しを最適化するために多くの作業が行われています。たとえば、R5RSは、(仕様では!)すべての実装が、スタックオーバーフローを心配せずに、バインドされていない末尾再帰呼び出しを処理することを要求しています。比較のために、デフォルトでは、Cコンパイラーは明らかな末尾呼び出しの最適化(リンクリストの再帰的な逆を試みます)も行いません。いくつかの呼び出しの後、プログラムは終了します(ただし、コンパイラーを使用すると、最適化されます- O2)。

もちろん、指数関数的である有名なfibの例のようにひどく書かれたプログラムでは、コンパイラーはその「魔法」を実行するオプションをほとんど、またはまったく持っていません。したがって、最適化におけるコンパイラの作業を妨げないように注意する必要があります。

編集:fibの例とは、以下を意味します。

(define (fib n)
 (if (< n 3) 1 
  (+ (fib (- n 1)) (fib (- n 2)))
 )
)
0
K.Steff

関数型言語は、テール再帰と無限再帰という2種類の非常に特殊な再帰が得意です。これらは、factorialの例のように、他の種類の再帰における他の言語と同じくらい悪いです。

それは両方のパラダイムで定期的な再帰でうまく機能するアルゴリズムがないと言っているわけではありません。たとえば、深さ優先ツリー検索など、スタックのようなデータ構造が必要な場合は、再帰を使用して実装するのが最も簡単です。

再帰は関数型プログラミングでより頻繁に登場しますが、特に初心者や初心者向けのチュートリアルでは、使いすぎていることもあります。おそらく、関数型プログラミングのほとんどの初心者は、命令型プログラミングの前に再帰を使用しているためです。リスト内包表記、高次関数、コレクションに対するその他の操作など、他の関数型プログラミング構成要素があり、スタイル、簡潔さ、効率性、および最適化機能のために、概念的にはるかに適しています。

たとえば、delnanのfactorial n = product [1..n]の提案は、より簡潔で読みやすいだけでなく、高度な並列化も可能です。 foldまたはreduceを使用する場合も同じで、言語にproductが組み込まれていない場合があります。再帰は、この問題の最後の解決策です。チュートリアルでそれが再帰的に解決されるのを見る主な理由は、ベストプラクティスの例としてではなく、より良いソリューションに到達する前の出発点としてです。

0
Karl Bielefeldt