web-dev-qa-db-ja.com

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

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

22
mrpyo

レイジネスやその他のトリックを利用して、一定の償却時間を実現する、または( queues などの限られたケースでは)一定の時間を更新して、さまざまな種類の問題を解決する、膨大な数のデータ構造があります。 Chris Okasakiの PhD論文 "純粋に機能的なデータ構造"と同じ名前の本は、最初の例(おそらく最初の主要なもの)ですが、 この分野はそれ以来進歩しています 。これらのデータ構造は通常、純粋にインターフェースで機能するだけでなく、純粋なHaskellおよび類似の言語で実装することもでき、完全に永続的です。

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

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

機能的な設定に変更可能な状態を安全に導入するための別のオプションは、HaskellのSTモナドです。ミューテーションなしで実装でき、unsafe*関数、それは振る舞う永続的なデータ構造を暗黙的に渡すことに関する単なるラッパーであるかのように動作します(Stateを参照)。ただし、評価の順序を強制し、エスケープを防ぐいくつかの型システムのトリックのため、インプレース変異を使用して安全に実装でき、すべてのパフォーマンス上の利点があります。

32
user7043

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

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

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

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

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

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

ヘッドノード(および拡張によってヘッドから始まる任意の部分)を安価に置き換えることで、不変のリンクリストを取得できます。新しいヘッドを、古いヘッドと同じ次のノードに向けるだけです。これは、多くのリスト処理アルゴリズム(foldベースのもの)でうまく機能します。

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

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

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

9
9000

あなたが求めていることは少し広すぎる。 O(1)どの位置からの除去と置換?シーケンスの先頭?テール?任意の位置?使用するデータ構造は、それらの詳細に依存します。それは 2-3フィンガーツリー は、最も汎用性の高い永続的なデータ構造の1つに見えます。

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

(...)

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

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

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

2
Doval