関数型言語は再帰に優れていますか?


41

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(および関数型言語全般)は、非関数型言語よりも再帰を頻繁に使用することを指摘したかっただけです。


10
適切な事例:多くの関数型言語はテールコールの最適化を多用していますが、それを行う手続き型言語はほとんどありません。つまり、これらの関数型言語では、末尾呼び出しの再帰がはるかに安価です。
ヨアヒムザウアー

7
実際、あなたが与えたHaskellの定義はかなり悪いです。factorial n = product [1..n]より簡潔で、より効率的で、大規模にスタックをオーバーフローさせませんn(メモ化が必要な場合は、まったく異なるオプションが必要です)。productいくつかの観点で定義さfoldれた、され再帰的に定義されていますが、細心の注意を払って。再帰ほとんどの場合許容できる解決策ですが、間違った/次善の方法を実行するのは依然として簡単です。

1
@JoachimSauer-少し装飾があれば、あなたのコメントは価値のある答えになるでしょう。
マークブース

あなたの編集は、あなたが私のドリフトをキャッチしなかったことを示しています。あなたが与えた定義は関数型言語でさえ悪い再帰の完全な例です。私の代替手段も再帰的であり(ライブラリー関数内にあります)、非常に効率的で、再帰する方法だけが違いを生みます。Haskellはまた、怠inessが通常の規則を破るという奇妙なケースです(ケースインポイント:関数は末尾再帰でありながらスタックをオーバーフローさせ、末尾再帰でなくても非常に効率的です)。

@delnan:説明をありがとう!編集内容を編集します;)
marco-fiset

回答:


36

はい、彼らはだけでなく、彼らはので、行うことができます彼らは、しかしために持っています

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

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

ただし、副作用がないということは、再帰をより効率的に実装でき、コンパイラが再帰をより積極的に最適化できることも意味します。私はそのようなコンパイラーについて詳しくは研究していませんが、私が知る限り、ほとんどの関数型プログラミング言語のコンパイラーは末尾呼び出しの最適化を実行し、一部のコンパイラーは特定の種類の再帰構造を背後でループにコンパイルすることさえあります。


2
記録のために、テールコールの除去は純度に依存しません。
スカーフリッジ

2
@scarfridge:もちろんそうではありません。ただし、純度が指定されている場合、コンパイラーはコードを並べ替えてテールコールを許可する方がはるかに簡単です。
tdammers

GCCはGHCよりもTCOの方がはるかに優れています。サンクの作成全体でTCOを実行できないためです。
dan_waterworth

18

再帰と反復を比較しています。末尾呼び出しの除去がなければ、余分な関数呼び出しがないので、反復は実際により効率的です。また、反復は永遠に続く可能性がありますが、関数呼び出しが多すぎるとスタック領域が不足する可能性があります。

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

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


私はすでにパターンマッチングのすべてを知っており、使用している言語では見逃しているHaskellの素晴らしい機能だと思います!
marco-fiset

@marcof私のポイントは、再帰と反復についてのすべての話が、コードサンプルの洗練さを扱っていないということです。本当にパターンマッチングと条件についてです。おそらく、答えの一番上に置くべきだったのでしょう。
12

はい、私もそれを理解しました:P
marco-fiset

@chrisaycock:ループ本体で使用されるすべての変数が引数であり、再帰呼び出しの戻り値であるテール再帰として反復を見ることができますか?
ジョルジオ

@Giorgio:はい、関数に同じタイプのタプルを取得させて返します。
Ericson2314

5

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

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

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

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

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

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


1
Re:1.アーリーリターンはテールコールとは関係ありません。テールコールで早期に戻り、「レイト」リターンにテールコールを含めることもできます。また、テールポジションにない再帰呼び出しで単一の単純な式を使用できます(OPの階乗の定義を参照)。

@delnan:ありがとう。それは早く、私がそのことを研究してからかなりの時間が経ちました。
テラスティン

1

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

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

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

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


5
Haskellは非常に少数の非厳密な言語の1 つです。ML ファミリ全体(遅延を追加するいくつかの研究スピンオフを除く)、すべての一般的なLisp、Erlangなどはすべて厳密です。また、2番目の段落はオフのようです-最初の段落で正しく述べているように、遅延無限再帰を許可ます(forever a = a >> forever aたとえば、Haskellプレリュードは非常に便利です)。

@deinan:私が知っている限りでは、SML / NJも遅延評価を提供しますが、SMLに追加されます。また、数少ない怠functionalな関数型言語のうちの2つ、MirandaとCleanに名前を付けたかったのです。
ジョルジオ

1

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

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


1
(1)このような最適化は非常に特定の場合にのみ適用されます-OPの例はそうではなく、他の多くの単純な関数は末尾再帰になるために特別な注意が必要です。(2)実末尾呼び出しの最適化は唯一の再帰関数を最適化していない、それはからスペースのオーバーヘッドを削除する任意の直後にリターンが続いているコール。

@delnan:(1)はい、非常に本当です。。この回答の私の「原案」で、私は(2)はい、しかし、質問の文脈の中で、私はそれが言及する余分だろうと思っていること:(言及していた
スティーブン・エヴァース

はい、(2)は便利な追加です(継続渡しのスタイルには不可欠ですが)、言及する必要のない答えはそうです。

1

あなたは見たいでしょうガベージコレクションの高速ですが、スタックが高速化され、CプログラマはコンパイルC. I内のスタックフレームのための「ヒープ」と考えるだろうか使用についての論文では、著者はそれを行うにはGCCで手を加え信じて。これは明確な答えではありませんが、再帰に関する問題のいくつかを理解するのに役立つかもしれません。

アレフプログラミング言語ベル研究所からプラン9と一緒に来るために使用される、「なる」の文(参照セクションの6.6.4持っていたこの参照を)。これは、一種の明示的な末尾呼び出し再帰最適化です。「しかし、それは呼び出しスタックを使い果たします!」再帰に対する議論は、潜在的に廃止される可能性があります。


0

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

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

編集:fibの例では、私は次のことを意味します:

(define (fib n)
 (if (< n 3) 1 
  (+ (fib (- n 1)) (fib (- n 2)))
 )
)

0

関数型言語は、末尾再帰と無限再帰の2つの非常に具体的な再帰で優れています。あなたのfactorial例のように、他の種類の再帰では他の言語と同じくらい悪いです。

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

関数型プログラミングでは再帰がより頻繁に発生しますが、特に初心者や初心者向けのチュートリアルでは、関数型プログラミングのほとんどの初心者が命令型プログラミングで再帰を使用したことがあるため、過度に使用されています。リスト内包表記、高階関数、コレクションに対する他の操作など、他の関数型プログラミング構成要素があります。これらは通常、概念、スタイル、簡潔さ、効率、最適化の能力により適しています。

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

弊社のサイトを使用することにより、あなたは弊社のクッキーポリシーおよびプライバシーポリシーを読み、理解したものとみなされます。
Licensed under cc by-sa 3.0 with attribution required.