web-dev-qa-db-ja.com

Haskell:リスト、配列、ベクター、シーケンス

私はHaskellを学んでおり、Haskellリストと(あなたの言語を挿入して)配列のパフォーマンスの違いに関する記事をいくつか読んでいます。

学習者であるため、明らかにパフォーマンスの違いを考えずにリストを使用しています。私は最近調査を開始し、Haskellで利用可能な多数のデータ構造ライブラリを見つけました。

データ構造のコンピューターサイエンス理論に深く入り込むことなく、リスト、配列、ベクター、シーケンスの違いを誰かが説明できますか?

また、あるデータ構造を別のデータ構造の代わりに使用する一般的なパターンはありますか?

私が行方不明で、役に立つかもしれない他の形式のデータ構造はありますか?

217
r.sendecky

リストロック

Haskellのシーケンシャルデータで最も使いやすいデータ構造はリストです

 data [a] = a:[a] | []

リストは、ϴ(1)の短所とパターンマッチングを提供します。標準ライブラリ、そしてそのことに関してプレリュードは、コード(foldrmapfilter)を散らかすべき便利なリスト関数でいっぱいです。リストはpersistantであり、別名純粋に機能的で、非常にいいです。 Haskellリストは、共誘導的であるため(他の言語ではこれらのストリームと呼ばれます)、実際には「リスト」ではありません。

ones :: [Integer]
ones = 1:ones

twos = map (+1) ones

tenTwos = take 10 twos

素晴らしく働きます。無限のデータ構造が揺れ動きます。

Haskellのリストは、命令型言語のイテレーターによく似たインターフェースを提供します(遅延のため)。したがって、それらが広く使用されていることは理にかなっています。

一方

リストの最初の問題は、リストにインデックスを付けるには(!!)がϴ(k)時間かかるということで、これは迷惑です。また、追加は遅い++になる可能性がありますが、Haskellの遅延評価モデルは、これらが発生した場合、完全に償却されたものとして扱うことができることを意味します。

リストの2番目の問題は、リストのデータの局所性が低いことです。実際のプロセッサは、メモリ内のオブジェクトが互いに隣り合って配置されていない場合、高い定数が発生します。したがって、C++ではstd::vectorは、私が知っている純粋なリンクリストデータ構造よりも高速な "snoc"(最後にオブジェクトを置く)を持ちます。

リストの3番目の問題は、スペース効率が悪いことです。余分なポインタの束(一定の要因で)ストレージを押し上げます。

シーケンスは機能的です

Data.Sequenceは内部的に 指の木 に基づいています(これを知りたくありません)。つまり、いくつかのNiceプロパティがあります。

  1. 純粋に機能します。 Data.Sequenceは完全に永続的なデータ構造です。
  2. ツリーの最初と最後にすばやくアクセスできます。 ϴ(1)(償却済み)は、最初または最後の要素を取得するか、ツリーを追加します。リストの処理速度が最速の場合、Data.Sequenceは常に遅くなります。
  3. log(log n)シーケンスの中央へのアクセス。これには、値を挿入して新しいシーケンスを作成することが含まれます
  4. 高品質のAPI

一方、Data.Sequenceはデータの局所性の問題に対してあまり機能せず、有限のコレクションに対してのみ機能します(リストよりも遅延が少ない)

アレイは心臓の弱い人向けではありません

配列は、CSで最も重要なデータ構造の1つですが、怠zyな純粋な機能的世界にはあまり適合しません。配列は、コレクションの中央へのϴ(1)アクセスと、非常に優れたデータの局所性/定数要因を提供します。しかし、それらはHaskellにうまく適合しないため、使用するのが面倒です。実際、現在の標準ライブラリにはさまざまな配列タイプが多数あります。これらには、完全永続配列、IOモナドの可変配列、STモナドの可変配列、および上記の非ボックス化バージョンが含まれます。詳細については the haskell wiki

ベクトルは「より良い」配列です

Data.Vectorパッケージは、より優れたよりクリーンなAPIで、配列のすべての良さを提供します。何をしているのか本当に理解していない限り、配列のようなパフォーマンスが必要な場合はこれらを使用する必要があります。もちろん、いくつかの注意事項がまだ適用されます。データ構造のような可変配列は、純粋な遅延言語ではニースを再生しません。それでも、O(1)のパフォーマンスが必要な場合があり、Data.Vectorは使用可能なパッケージで提供します。

他のオプションがあります

最後に効率的に挿入できるリストが必要な場合は、 difference list を使用できます。パフォーマンスを台無しにするリストの最良の例は、プレリュードがStringとしてエイリアスした[Char]から来る傾向があります。 Charリストは便利ですが、C文字列の20倍の速度で実行される傾向があるため、Data.Textまたは非常に高速なData.ByteStringを自由に使用してください。私が今考えていない他のシーケンス指向のライブラリがあると確信しています。

結論

Haskellリストでシーケンシャルコレクションが必要な時間の90%以上が適切なデータ構造です。リストはイテレータのようなもので、リストを使用する関数は、付属のtoList関数を使用して、これらの他のデータ構造で簡単に使用できます。より良い世界では、プレリュードは、使用するコンテナの種類に関して完全にパラメトリックになりますが、現在は標準ライブラリに[]が散在しています。したがって、リストを(ほぼ)すべての場所で使用することは間違いなく大丈夫です。
ほとんどのリスト関数の完全にパラメトリックなバージョンを入手できます(そしてそれらを使用する気高い)

Prelude.map                --->  Prelude.fmap (works for every Functor)
Prelude.foldr/foldl/etc    --->  Data.Foldable.foldr/foldl/etc
Prelude.sequence           --->  Data.Traversable.sequence
etc

実際、Data.Traversableは、「リストのような」もの全体で多かれ少なかれ普遍的なAPIを定義します。

それでも、あなたは上手く、完全にパラメトリックなコードだけを書くことができますが、私たちのほとんどはそうではなく、あちこちでリストを使用しています。あなたが学んでいるなら、あなたもそうすることを強くお勧めします。


編集:Data.VectorData.Sequenceを使用するタイミングを説明したことがないことに気づいたコメントに基づきます。配列とベクターは、非常に高速なインデックス作成とスライス操作を提供しますが、基本的には一時的な(必須の)データ構造です。 Data.Sequence[]などの純粋な機能データ構造により、古い値を変更したかのように、古い値からnew値を効率的に生成できます。 。

  newList oldList = 7 : drop 5 oldList

古いリストを変更せず、コピーする必要もありません。したがって、oldListが非常に長い場合でも、この「変更」は非常に高速です。同様に

  newSequence newValue oldSequence = Sequence.update 3000 newValue oldSequence 

3000要素の代わりにnewValueを持つ新しいシーケンスを生成します。繰り返しますが、古いシーケンスを破壊するのではなく、新しいシーケンスを作成するだけです。しかし、これは非常に効率的に行われ、O(log(min(k、k-n))を取得します。nはシーケンスの長さで、kは変更するインデックスです。

VectorsArraysを使用してこれを簡単に行うことはできません。それらはmodifiedでもかまいませんが、それは本当の命令的な修正であり、通常のHaskellコードではできません。つまり、Vectorsnocなどの変更を行うconsパッケージ内の操作は、ベクトル全体をコピーする必要があるため、O(n)時間かかります。唯一の例外は、STモナド(またはIO)内で可変バージョン(Vector.Mutable)を使用し、命令型言語の場合と同じようにすべての変更を実行できることです。完了したら、ベクターを「フリーズ」して、純粋なコードで使用する不変の構造に変換します。

リストが適切でない場合は、デフォルトでData.Sequenceを使用する必要があります。 Data.Vectorは、使用パターンに多くの変更が含まれない場合、またはST/IOモナド内で非常に高いパフォーマンスが必要な場合にのみ使用してください。

STモナドのすべての話が混乱している場合:純粋で高速で美しいData.Sequenceに固執する理由がさらに多くあります。

325
Philip JF