純粋に機能的なプログラミング言語は、急速に変化するデータをどのように処理しますか?


22

O(1)の削除と置換を取得できるように、どのデータ構造を使用できますか?または、前述の構造が必要な状況をどのように回避できますか?


2
純粋に関数型のプログラミング言語に精通していない私たちにとって、あなたの問題が何であるかを理解するために、もう少し背景を提供してもらえますか?
FrustratedWithFormsDesigner 14

4
@FrustratedWithFormsDesigner純粋に機能的なプログラミング言語では、すべての変数が不変である必要があり、「変更」されると新しいバージョンを作成するデータ構造が必要になります。
ドーバル14

5
純粋に機能的なデータ構造に関する岡崎の仕事を知っていますか?

2
可能性の1つは、可変データ用のモナドを定義することです(例:haskell.org/ghc/docs/4.08/set/sec-marray.htmlを参照)。このようにして、可変データはIOと同様に扱われます。
ジョルジオ14

1
@CodesInChaos:ただし、このような不変の構造は、通常、単純な配列よりもはるかにオーバーヘッドが大きくなります。その結果、実際に大きな違いがあります。そのため、汎用プログラミングを目的とする純粋に機能的な言語には、純粋なセマンティクスと互換性のある安全な方法で、可変構造を使用する方法が必要です。STHaskell のモナドはこれをうまく行います。
左辺約14

回答:


32

怠inessやその他のトリックを活用して、償却された一定時間、または(キューなどの一部の限られたケースでは)さまざまな種類の問題に対する一定時間の更新を実現する膨大なデータ構造があります。クリス・オカサキの博士論文「Purely Functional Data Structures」と同名の本は(おそらく最初の主要なものの)代表的な例ですが、それ以来この分野は進歩しています。これらのデータ構造は通常、インターフェースで純粋に機能するだけでなく、純粋なHaskellおよび同様の言語で実装することもでき、完全に永続的です。

これらの高度なツールがなくても、単純なバランスの取れたバイナリ検索ツリーは対数時間の更新を提供するため、可変メモリは最悪の場合は対数スローダウンでシミュレートできます。

不正行為と見なされる可能性のある他のオプションもありますが、実装作業と実際のパフォーマンスに関しては非常に効果的です。たとえば、線形型または一意性型は、プログラムが前の値(変更されるメモリ)を保持しないようにすることで、概念的に純粋な言語の実装戦略としてインプレース更新を許可します。これは、永続的なデータ構造ほど一般的ではありません。たとえば、以前のバージョンの状態をすべて保存して、アンドゥログを簡単に作成することはできません。AFAIKはまだ主要な機能言語では利用できませんが、それはまだ強力なツールです。

可変状態を機能設定に安全に導入するためのもう1つのオプションは、STHaskell のモナドです。ミューテーションなしで実装でき、unsafe*機能がなければ、永続的なデータ構造を暗黙的に渡すことの単なる派手なラッパーのように動作します(cf. State)。しかし、評価の順序を強制し、エスケープを防ぐタイプシステムトリックにより、インプレースミューテーションを使用して安全に実装でき、すべてのパフォーマンス上の利点があります。


また、あなたのリストまたはツリー内の焦点に急速な変化を行う能力を与える言及する価値があるかもしれないジッパー
JKを。

1
@jk。それらは、私がリンクしたTheoretical Computer Scienceの投稿で言及されています。さらに、これらは多くの関連データ構造の1つ(まあ、クラス)にすぎず、それらのすべてを議論することは範囲外であり、ほとんど使用されません。

結構、リンクをたどっていませんでした
jk。

9

1つの安価な可変構造は、引数スタックです。

典型的なSICPスタイルの階乗計算を見てください。

(defn fac (n accum) 
    (if (= n 1) 
        accum 
        (fac (- n 1) (* accum n)))

(defn factorial (n) (fac n 1))

ご覧のとおり、への2番目の引数facは、急速に変化する積を含む可変アキュムレーターとして使用されますn * (n-1) * (n-2) * ...。ただし、変更可能な変数はありません。また、アキュムレータを別のスレッドなどから誤って変更する方法はありません。

もちろんこれは限定的な例です。

ヘッドノードを安価に交換することで、不変のリンクリストを取得できます(さらに、ヘッドから始まる部分を拡張することで)。古いヘッドと同じ次のノードを新しいヘッドポイントに設定するだけです。これは、多くのリスト処理アルゴリズム(何でもfoldベース)でうまく機能します。

たとえばHAMTに基づいた連想配列からかなり良いパフォーマンスを得ることができます。論理的に、いくつかのキーと値のペアが変更された新しい連想配列を受け取ります。実装は、古いオブジェクトと新しく作成されたオブジェクトの間でほとんどの共通データを共有できます。ただし、これはO(1)ではありません。通常、少なくとも最悪の場合、対数的なものが得られます。一方、不変ツリーは、通常、可変ツリーと比較してパフォーマンスが低下することはありません。もちろん、これにはある程度のメモリオーバーヘッドが必要です。

別のアプローチは、木が森に落ちて誰もそれを聞いていない場合、音を出す必要がないという考えに基づいています。つまり、少し変化した状態が何らかのローカルスコープを決して離れないことを証明できれば、その中のデータを安全に変更できます。

Clojureには、ローカルスコープの外部にリークしない不変のデータ構造の可変の「シャドウ」であるトランジェントがあります。CleanはUniquesを使用して同様のことを実現します(正しく覚えている場合)。Rustは、静的にチェックされた一意のポインターで同様のことを行うのに役立ちます。


1
+1、また、クリーンで一意のタイプを言及するため。
ジョルジオ14

@ 9000 HaskellにはClojureのトランジェントに似たものがあると聞いたと思います。間違っていれば誰かが私を修正します。
ポール14

@paul:私はHaskellについて非常に大まかな知識を持っているので、もしあなたが私の情報(少なくともgoogleのキーワード)を提供できれば、喜んで答えへの参照を含めたいと思います。
9000 14

1
@paulよく分からない。しかし、HaskellにはMLに似たものを作成refし、それらを特定のスコープ内にバインドする方法があります。IORefまたはを参照してくださいSTRef。そしてもちろん、似たようなTVarsとMVarsがありますが、コンカレントセマンティクスは同じです(TVarsのstmとsのmutexベースMVar
Daniel Gratzer 14

2

あなたが求めているのは少し広すぎる。O(1)どの位置からの取り外しと交換?シーケンスの頭?しっぽ?任意の位置?使用するデータ構造は、それらの詳細に依存します。ただし、2〜3本のフィンガーツリーは、最も汎用性の高い永続的なデータ構造の1つと思われます。

2〜3本のフィンガーツリーを提供します。これは、償却された一定の時間での端へのアクセスをサポートする永続的なシーケンスの機能的表現と、小さなピースのサイズでの時間の対数の連結と分割をサポートします。

(...)

さらに、分割操作を一般的な形式で定義することにより、シーケンス、優先度キュー、検索ツリー、優先度検索キューなどとして機能できる汎用データ構造を取得します。

一般に、永続的なデータ構造は、任意の位置を変更するときに対数パフォーマンスを発揮します。O(1)アルゴリズムの定数は高い可能性があり、対数のスローダウンはより遅いアルゴリズム全体に「吸収」される可能性があるため、これは問題になる場合もあれば、そうでない場合もあります。

さらに重要なことは、永続的なデータ構造により、プログラムに関する推論が容易になることであり、これが常にデフォルトの操作モードである必要があります。永続的なデータ構造を可能な限り優先し、永続的なデータ構造がパフォーマンスのボトルネックであることをプロファイリングして決定した後にのみ、可変のデータ構造を使用する必要があります。それ以外は早すぎる最適化です。

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