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


230

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

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

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

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

私が見逃していて、役に立つかもしれない他の形式のデータ構造はありますか?


1
リストと配列については、この回答をご覧ください。
GrzegorzChrupała12年

ここでもData.Mapについて説明するのは良いことです。これは、特に多次元データの場合に便利なデータ構造のようです。
Martin Capodici

回答:


339

リスト・ロック

Haskellのシーケンシャルデータで最もわかりやすいデータ構造は、リストです。

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

リストは、ϴ(1)の短所とパターンマッチングを提供します。プレリュードライブラリ、およびそのことについての標準は、そのごみあなたのコード(はず便利なリスト機能がいっぱいですfoldrmapfilter)。リストは永続的で、純粋に機能的です。これはとても素晴らしいことです。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内部的にはフィンガーツリーに基づいています(これは知りたくありません)。これは、いくつかの優れたプロパティがあることを意味します

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

一方、Data.Sequenceデータの局所性の問題にはあまり効果がなく、有限のコレクションに対してのみ機能します(リストよりも面倒ではありません)。

配列は気弱な人には向いていません

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

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

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

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

末尾に効率的に挿入する機能を備えたリストが必要な場合は、差分リストを使用できます。パフォーマンスを台無しにするリストの最良の例は[Char]、プレリュードがとしてエイリアス化したものに由来する傾向がありますStringCharリストは便利ですが、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.Vectorvs を使用するかについて説明したことがないことを理解しましたData.Sequence。配列とベクトルは、非常に高速なインデックス作成とスライス操作を提供しますが、基本的に一時的な(命令型)データ構造です。以下のような純粋な関数型データ構造Data.Sequence[]効率的に生成させ、新たな古い値を変更したかのように古い値から値を。

  newList oldList = 7 : drop 5 oldList

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

  newSequence newValue oldSequence = Sequence.update 3000 newValue oldSequence 

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

Vectorsとでこれを簡単に行うことはできませんArrays。それらは変更できますが、これは実際に必須の変更であるため、通常のHaskellコードでは実行できません。手段の操作そのVectorようなパッケージそのメイクの修正snocconsテイクので、全体のベクトルをコピーする必要がありO(n)、時間。これの唯一の例外はVector.MutableSTモナド(またはIO)内で可変バージョン()を使用して、命令型言語で行うのと同じようにすべての変更を行うことができることです。完了したら、ベクターを「フリーズ」して、純粋なコードで使用する不変の構造に変換します。

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

このSTモナドの話がすべてあなたを混乱させているなら、純粋に速くて美しいことに固執する理由はなおさらありますData.Sequence


45
私が聞いた洞察の1つは、リストは基本的にHaskellのデータ構造と同じくらい制御構造であるということです。これは理にかなっています。別の言語でCスタイルのforループを使用する場合は[1..]、Haskellでリストを使用します。リストは、バックトラックなどの楽しいことにも使用できます。それらを制御構造(一種)として考えることは、それらがどのように使用されるかを理解するのに本当に役立ちました。
Tikhon Jelvis 2012年

21
すばらしい答えです。私の唯一の不満は、「シーケンスは機能している」がそれらを少しアンダーセルしているということです。シーケンスは機能的な素晴らしいソースです。それらに対するもう1つのボーナスは、高速な結合と分割(log n)です。
Dan Burton

3
@ダンバートンフェア。私はおそらくアンダーセルをしましたData.Sequence。フィンガーツリーは、コンピューティングの歴史の中で最も素晴らしい発明の1つであり(ギバスはおそらくいつかチューリング賞を受賞するはずData.Sequenceです)、優れた実装であり、非常に使いやすいAPIを備えています。
フィリップJF 2012年

3
あなたがもしので、興味深い文言を「.. UseData.Vectorは、あなたの使用パターンは、多くの変更を加える必要としない場合、またはあなたがST / IOモナド内で非常に高いパフォーマンスを必要とする場合は、」している繰り返しのような多くの変更((100K回)を作ります100k要素の進化)、許容できるパフォーマンスを得るにはST / IO Vector 必要です
misterbee

4
(純粋な)ベクターとコピーに関する懸念は、ストリームの融合によって部分的に緩和されます。たとえば、これimport qualified Data.Vector.Unboxed as VU; main = print (VU.cons 'a' (VU.replicate 100 'b'))は、コアで404バイト(101文字)の単一割り当てにコンパイルされます:hpaste.org/65015
FunctorSalad
弊社のサイトを使用することにより、あなたは弊社のクッキーポリシーおよびプライバシーポリシーを読み、理解したものとみなされます。
Licensed under cc by-sa 3.0 with attribution required.