純粋に関数型プログラミングの効率


397

命令型ではなく純粋に機能的にプログラミングするときに発生する可能性がある最悪の漸近的なスローダウンが何であるかを誰かが知っていますか(つまり、副作用を許可する)。

itowlsonによるコメントからの明確化:最もよく知られている非破壊アルゴリズムが最もよく知られている破壊アルゴリズムよりも漸近的に悪い問題はありますか?


6
命令型プログラミングの場合と同じです。
R.マルティーニョフェルナンデス2010

3
@jldupont:もちろん計算の結果を返すため。多くの副作用のないプログラムが存在します。彼らは、入力を計算する以外に多くのことはできません。しかし、それはまだ役に立ちます。
10

24
私の機能的なコードをひどく書くことで、あなたが好きなだけ悪くすることができます!*にやにや*あなたが尋ねていることは、「最もよく知られている非破壊アルゴリズムが最もよく知られている破壊アルゴリズムよりも漸近的に悪い問題はありますか?」 ?
itowlson、2010年

2
あなたが興味を持っている減速の種類の例を挙げてもらえますか。あなたの質問は少しあいまいです。
Peter Recore

5
ユーザーは自分の答えを削除しましたが、8クイーンの問題の機能バージョンがn = 13で1分以上かかったと主張しました。彼はそれが「非常にうまく書かれていない」と認めたので、私は自分のバージョンのF#の8つのクイーン:pastebin.com/ffa8d4c4。言うまでもありませんが、私の純粋な関数プログラムは、n = 20を1秒強で計算します。
Juliet

回答:


531

Pippenger [1996]によると、純粋に機能する(そして、レイジーではなく、厳密な評価セマンティクスを持つ)Lispシステムを、データを変更できるものと比較すると、O(n)で実行される不純なLisp用に書かれたアルゴリズムを変換できます。 O(n log n)時間で実行される純粋なLispのアルゴリズムへ(ポインターのみを使用したランダムアクセスメモリのシミュレーションに関するBen-AmramおよびGalil [1992]の作業に基づく)。Pippengerはまた、それがあなたができる最善のアルゴリズムがあることを確立しています。純粋なシステムではΩ(n log n)である不純なシステムではO(n)である問題があります。

このペーパーについては、いくつかの注意事項があります。最も重要なのは、Haskellなどの遅延関数型言語に対応していないことです。Bird、Jones and De Moor [1997]は、Pippengerによって構築された問題が遅延関数型言語でO(n)時間で解決できることを示していますが、かどうかにかかわらず(また、私が知る限り、誰も持っていません)怠惰な関数型言語では、突然変異のある言語と同じ漸近実行時間ですべての問題を解決できません。

Pippengerによって構築された問題は、Ω(n log n)がこの結果を達成するために特別に構築されている必要があり、必ずしも実際の現実の問題を表すものではありません。問題には少し予想外の制限がいくつかありますが、証明が機能するために必要です。特に、この問題では、将来の入力にアクセスできずに結果がオンラインで計算され、入力が固定サイズセットではなく、無制限の可能な原子のセットからの原子のシーケンスで構成されている必要があります。また、この論文では、線形実行時間の不純なアルゴリズムの結果(下限)のみを確立しています。より長い実行時間を必要とする問題の場合、追加のO(log n)線形問題で見られる要因は、実行時間が長いアルゴリズムに必要な追加の操作のプロセスで「吸収」される可能性があります。これらの説明と未解決の質問は、Ben-Amram [1996]によって簡潔に検討されています。

実際には、多くのアルゴリズムは、変更可能なデータ構造を持つ言語と同じ効率で、純粋な関数型言語で実装できます。純粋に機能的なデータ構造を効率的に実装するために使用する手法の適切なリファレンスについては、Chris Okasakiの「Purely Functional Data Structures」[Okasaki 1998](彼の論文[Okasaki 1996]の拡張版)を参照してください。

純粋に機能するデータ構造にアルゴリズムを実装する必要がある人は、岡崎を読むべきです。バランスの取れたバイナリツリーで可変メモリをシミュレートすることで、操作ごとに常に最悪の場合O(log n)のスローダウンを得ることができますが、多くの場合、それよりもはるかに優れています。償却作業を段階的に行う時間的なもの。純粋に機能的なデータ構造は、操作や分析が少し難しい場合がありますが、コンパイラーの最適化、並列分散コンピューティング、バージョン管理、元に戻す、ロールバックなどの機能の実装に役立つ参照透過性など、多くの利点があります。

これはすべて、漸近的な実行時間についてのみ説明していることにも注意してください。純粋に機能的なデータ構造を実装するための多くの手法は、それらが機能するために必要な追加の簿記と問題の言語の実装の詳細により、一定量の一定の速度低下をもたらします。純粋に機能的なデータ構造の利点は、これらの一定の要因による減速を上回る可能性があるため、通常は問題の問題に基づいてトレードオフを行う必要があります。

参考文献


50
Pippingerは、この問題について議論の余地のない権威です。ただし、彼の結果は理論的なものであり、実際的なものではないことを強調しておく必要があります。機能的データ構造を実用的かつ効率的にすることに関して言えば、岡崎に勝るものはありません。
Norman Ramsey、2010年

6
itowlson:私はあなたの質問に答えるのに十分なピッペンジャーを読まなかったことを認めなければなりません。それは岡崎が引用した査読付きジャーナルに掲載されました、そして私は彼の主張がこの質問に関連していることを決定するのに十分にそれを読みました、しかし証拠を理解するのに十分ではありません。現実の結果について私がすぐに得られることは、バランスの取れたバイナリツリーを使用して変更可能なメモリを単にシミュレートするだけで、O(n)不純なアルゴリズムをO(n log n)の純粋なアルゴリズムに変換することは簡単なことです。それ以上のことはできない問題があります。それらが純粋に理論的なものかどうかはわかりません。
ブライアンキャンベル

3
ピッペンジャーの結果は、その範囲を制限する2つの重要な仮定を行います。「オンライン」または「反応」計算(有限入力を単一の出力にマッピングする計算の通常のモデルではない)と、入力がシーケンスである「シンボリック」計算を考慮します。アトム。等しいかどうかのみテストできます(つまり、入力の解釈は非常に原始的です)。
Chris Conway

2
非常に良い答えです。純粋な関数型言語の場合、計算の複雑さについて普遍的に合意されたモデルはありませんが、不純な世界では、単価のRAMマシンが比較的標準であるため、物事の比較が難しくなります。また、純粋/不純のLg(N)の違いの上限は、純粋な言語での配列の実装を見ると、直感的に非常に簡単に説明できます(操作ごとにlg(n)がかかります(そして、履歴を取得します))。 。
user51568 2010年

4
重要な点:純粋に機能的な仕様をより複雑で効率的な純粋に機能な実装に手間をかけて変換しても、最終的に-自動または手動で-より効率的な不純なコードに変換する場合は、ほとんどメリットがありません。不純物をケージに入れておくことができれば(たとえば、外部からの副作用のない関数に閉じ込めることによって)、それほど重要ではありません。
ロビングリーン

44

確かにいくつかのアルゴリズムとデータ構造があり、そのために、遅延性があっても、漸近的に効率的な純粋に機能的なソリューション(純粋なラムダ計算で実装可能な最初のソリューション)は知られていません。

  • 前述のユニオン検索
  • ハッシュテーブル
  • 配列
  • 一部のグラフアルゴリズム
  • ...

ただし、「命令型」言語ではメモリへのアクセスはO(1)であると想定していますが、理論的にはこれは漸近的に(つまり、無限の問題サイズの場合)は不可能であり、巨大なデータセット内のメモリへのアクセスは常にO(log n)です。 、関数型言語でエミュレートできます。

また、現代のすべての関数型言語は変更可能なデータを提供し、Haskellは純粋さを犠牲にすることなくそれを提供することも覚えておく必要があります(STモナド)。


3
データセットが物理メモリに収まる場合、データへのアクセスはO(1)であり、アイテムを読み取る時間の絶対的な上限を見つけることができます。データセットがそうでない場合は、I / Oについて話しており、それが圧倒的に主要な要素になりますが、プログラムは作成されています。
ドナルフェロー

もちろん、私は外部メモリへのアクセスのO(log n)操作について話しています。ただし、いずれにせよ、私はbsについて話していました。外部メモリはO(1)でアドレス指定することもできます...
jkff

2
関数型プログラミングと比較して命令型プログラミングの最大の利点の1つは、ある状態のさまざまな側面への参照を保持し、それらのすべての参照が新しい状態の対応する側面を指すように新しい状態を生成できることです。関数型プログラミングを使用するには、現在の全体的な状態の特定のバージョンの適切な側面を見つけるために、直接逆参照操作をルックアップ操作に置き換える必要があります。
スーパーキャット2013年

ポインタモデル(大まかに言えばO(log n)メモリアクセス)でさえ、非常に大きなスケールでは物理的に現実的ではありません。光の速度は、異なるコンピューティング機器が互いに通信する速さを制限しますが、現在、特定の領域に保持できる情報の最大量は、その表面積によって制限されると考えられています。
dfeuer

36

この記事、union-findアルゴリズムの既知の純粋に機能的な実装はすべて、それらが公開しているものよりも漸近的な複雑さを持っていると主張します。

他の回答が違いは決してないと主張しているという事実と、たとえば、純粋に機能的なコードの唯一の「欠点」は、並列化できるということであり、これらの問題についての関数型プログラミングコミュニティの情報/客観性のアイデアを提供します。

編集:

以下のコメントは、純粋な関数型プログラミングの長所と短所の偏った議論が「関数型プログラミングコミュニティ」からのものではない可能性があることを指摘しています。いい視点ね。おそらく、私が見ている支持者たちは、コメントを引用すると、「読み書きができない」だけなのかもしれません。

たとえば、このブログの投稿は、関数型プログラミングコミュニティの代表と言える人物によって書かれたものだと思います。「遅延評価のポイント」のリストなので、欠点について言及するのに適しています。レイジーで純粋に関数型のプログラミングにはあるかもしれません。良い場所は、次の(技術的には真実ですが、面白くないという点に偏っています)解任の場所にあったでしょう。

厳密な関数が厳密な言語でO(f(n))の複雑さを持つ場合、遅延言語でも複雑さO(f(n))になります。なぜ心配ですか?:)


4

メモリ使用量の上限が固定されているため、違いはありません。

証明のスケッチ:メモリ使用量の上限が固定されている場合、あたかも実際にそのマシンで実行しているかのように同じ漸近的な複雑さで命令命令セットを実行する仮想マシンを作成できるはずです。これは、可変メモリを永続的なデータ構造として管理し、O(log(n))に読み取りと書き込みを与えることができるためですが、メモリ使用量の上限が固定されているため、メモリ量を固定して、 O(1)への崩壊。したがって、機能の実装は、VMの機能の実装で実行される命令バージョンになる可能性があるため、どちらも同じ漸近的な複雑さを持つ必要があります。


6
メモリ使用量の固定上限は、人々がこれらの種類のものを分析する方法ではありません。あなたは任意に大きいが有限のメモリを想定しています。アルゴリズムを実装するとき、最も単純な入力から任意の入力サイズまでどのようにスケーリングするかに興味があります。メモリ使用量に固定上限を設定する場合、計算にかかる時間を固定上限に設定して、すべてがO(1)であると言ってみませんか?
ブライアンキャンベル

@ブライアンキャンベル:それは本当です。必要であれば、実際には多くの場合、定数係数の違いを無視できることをお勧めします。メモリと時間の妥協の際には、m倍多くのメモリを使用するとランタイムが少なくともlog(m)だけ減少することを確認するために、違いに注意する必要があります。
Brian

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