リスト・ロック
Haskellのシーケンシャルデータで最もわかりやすいデータ構造は、リストです。
data [a] = a:[a] | []
リストは、ϴ(1)の短所とパターンマッチングを提供します。プレリュードライブラリ、およびそのことについての標準は、そのごみあなたのコード(はず便利なリスト機能がいっぱいですfoldr
、map
、filter
)。リストは永続的で、純粋に機能的です。これはとても素晴らしいことです。Haskellのリストは共和的であるため(他の言語ではこれらのストリームと呼ばれています)、実際には「リスト」ではありません。
ones :: [Integer]
ones = 1:ones
twos = map (+1) ones
tenTwos = take 10 twos
素晴らしく働きます。無限のデータ構造が揺れ動く。
Haskellのリストは、命令型言語のイテレータと非常に似たインターフェースを提供します(遅延のため)。したがって、それらが広く使用されていることは理にかなっています。
一方
リストの最初の問題は、リストにインデックスを(!!)
付けるのにϴ(k)時間を要し、煩わしいことです。また、追加は遅くなる可能性があります++
が、Haskellの遅延評価モデルは、これらが発生した場合でも、完全に償却されたものとして処理できることを意味します。
リストの2番目の問題は、リストのデータ局所性が低いことです。メモリ内のオブジェクトが互いに隣接して配置されていない場合、実際のプロセッサは高い定数を発生します。したがって、C ++では、std::vector
私が知っている純粋なリンクリストのデータ構造よりも "snoc"(オブジェクトを最後に置く)の方が高速ですが、これは永続的なデータ構造ではないため、Haskellのリストほど使いやすくありません。
リストの3番目の問題は、リストのスペース効率が悪いことです。余分なポインターの束がストレージを押し上げます(一定の要因による)。
シーケンスは機能している
Data.Sequence
内部的にはフィンガーツリーに基づいています(これは知りたくありません)。これは、いくつかの優れたプロパティがあることを意味します
- 純粋に機能的。
Data.Sequence
完全に永続的なデータ構造です。
- ツリーの最初と最後にすばやくアクセスできます。ϴ(1)(償却済み)。最初または最後の要素を取得するか、ツリーを追加します。リストが最も速いということで
Data.Sequence
は、多くても常に遅いです。
- log(log n)シーケンスの途中へのアクセス。これには、新しいシーケンスを作成するための値の挿入が含まれます
- 高品質のAPI
一方、Data.Sequence
データの局所性の問題にはあまり効果がなく、有限のコレクションに対してのみ機能します(リストよりも面倒ではありません)。
配列は気弱な人には向いていません
配列は、CSで最も重要なデータ構造の1つですが、遅延のある純粋な関数型の世界にはあまり適合しません。配列は、コレクションの中央へのϴ(1)アクセスと、非常に優れたデータの局所性/一定の要因を提供します。しかし、それらはHaskellにうまく適合しないため、使用するのは面倒です。現在の標準ライブラリには、実際には多数の異なる配列タイプがあります。これらには、完全に永続的な配列、IOモナドの可変配列、STモナドの可変配列、および上記のボックス化されていないバージョンが含まれます。詳細については、haskell wikiをご覧ください
ベクトルは「より良い」配列です
このData.Vector
パッケージは、すべての配列の良さを、より高レベルでよりクリーンなAPIで提供します。本当に何をしているのかを理解していない限り、パフォーマンスのような配列が必要な場合は、これらを使用する必要があります。もちろん、いくつかの警告が依然として適用されます。データ構造のような可変配列は、純粋な遅延言語ではうまく機能しません。それでも、そのO(1)パフォーマンス Data.Vector
が必要な場合があり、それを使用可能なパッケージで提供します。
他のオプションがあります
末尾に効率的に挿入する機能を備えたリストが必要な場合は、差分リストを使用できます。パフォーマンスを台無しにするリストの最良の例は[Char]
、プレリュードがとしてエイリアス化したものに由来する傾向がありますString
。 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.Vector
vs を使用するかについて説明したことがないことを理解しましたData.Sequence
。配列とベクトルは、非常に高速なインデックス作成とスライス操作を提供しますが、基本的に一時的な(命令型)データ構造です。以下のような純粋な関数型データ構造Data.Sequence
と[]
効率的に生成させ、新たな古い値を変更したかのように古い値から値を。
newList oldList = 7 : drop 5 oldList
古いリストを変更したり、コピーしたりする必要はありません。したがってoldList
、信じられないほど長い場合でも、この「変更」は非常に高速になります。同様に
newSequence newValue oldSequence = Sequence.update 3000 newValue oldSequence
newValue
3000要素の代わりにforを使用して新しいシーケンスを生成します。繰り返しますが、古いシーケンスを破壊するのではなく、新しいシーケンスを作成するだけです。ただし、これは非常に効率的に行われ、O(log(min(k、kn))を取得します。nはシーケンスの長さ、kは変更するインデックスです。
Vectors
とでこれを簡単に行うことはできませんArrays
。それらは変更できますが、これは実際に必須の変更であるため、通常のHaskellコードでは実行できません。手段の操作そのVector
ようなパッケージそのメイクの修正snoc
とcons
テイクので、全体のベクトルをコピーする必要がありO(n)
、時間。これの唯一の例外はVector.Mutable
、ST
モナド(またはIO
)内で可変バージョン()を使用して、命令型言語で行うのと同じようにすべての変更を行うことができることです。完了したら、ベクターを「フリーズ」して、純粋なコードで使用する不変の構造に変換します。
私の考えでData.Sequence
は、リストが適切でない場合はデフォルトで使用するべきです。使用Data.Vector
パターンに多くの変更を加えない場合、またはST / IOモナド内で非常に高いパフォーマンスが必要な場合にのみ使用してください。
このST
モナドの話がすべてあなたを混乱させているなら、純粋に速くて美しいことに固執する理由はなおさらありますData.Sequence
。