短所リストが関数型プログラミングに関連付けられているのはなぜですか?


22

ほとんどの関数型言語は、最も基本的なリストタイプとして、一重リンクリスト(「コン」リスト)を使用していることに気付きました。例には、Common Lisp、Haskell、およびF#が含まれます。これは、ネイティブリストタイプが配列であるメインストリーム言語とは異なります。

何故ですか?

Common Lisp(動的に型指定される)の場合、短所はリスト、ツリーなどのベースにもなるほど十分に一般的であるという考えがあります。これは小さな理由かもしれません。

ただし、静的に型付けされた言語の場合、正当な理由を見つけることができず、反論さえ見つけることができます。

  • 機能的なスタイルは不変性を促進するため、リンクリストの挿入の容易さはあまり利点ではありませんが、
  • 機能的なスタイルは不変性を促進し、データ共有も促進します。配列はリンクリストよりも「部分的に」共有しやすいため、
  • 同様に、通常の配列でパターンマッチングを行うこともできますし、さらに優れています(たとえば、右から左に簡単に折りたたむことができます)。
  • さらに、ランダムアクセスが無料で得られます。
  • そして(実用的な利点)言語が静的に型付けされている場合、通常のメモリレイアウトを使用して、キャッシュの速度を上げることができます。

では、なぜリンクリストを好むのでしょうか?


4
@ sepp2kの答えに対するコメントから、an array is easier to share "partially" than a linked listあなたが何を意味するのかを明確にする必要があると思います。私は理解しているように、その再帰的な性質のため、反対は真実です-配列は新しいコピーを作成するのに時間を費やす必要がありますが、その中のノードを渡すことで簡単にリンクリストを部分的に共有できます または、データ共有の観点から、2つのリンクリストは同じサフィックスを指すことができますが、配列では単純なことはできません。
イズカタ

配列が自身をオフセット、長さ、バッファートリプルとして定義する場合、オフセット+1、長さ-1、バッファーで新しい配列を作成して配列を共有できます。または、特別なタイプの配列をサブアレイとして使用します。
ドーベスヴァンダーメール

@Izkata配列について話すとき、Cの連続メモリの開始点へのポインタなどのバッファを意味することはめったにありません。通常、長さとラップされたバッファの開始点へのポインタを格納する何らかの構造を意味します。このようなシステムでは、スライス操作はサブ配列を返すことができ、そのバッファーポインターはバッファーの途中(サブ配列の最初の要素)を指し、カウントはstart + countが最後の要素を与えるようなものです。このようなスライシング操作は、時間と空間のO(1)
アレクサンダー-モニカーの復活

回答:


22

最も重要な要素は、O(1)時間で不変の片方向リンクリストの先頭に追加できることです。これにより、O(n)時間でn要素のリストを次のように再帰的に作成できます。

// Build a list containing the numbers 1 to n:
foo(0) = []
foo(n) = cons(n, foo(n-1))

不変配列を使用してこれをcons行うと、各操作で配列全体をコピーする必要があり、実行時間が2次になるため、ランタイムは2次になります。

機能的なスタイルは不変性を促進し、データ共有も促進します。配列はリンクリストよりも「部分的に」共有しやすい

「部分的に」共有するということは、O(1)時間で配列からサブ配列を取得できることを意味しますが、リンクリストではO(1)時間でしかテールを取得できず、他のすべてはO(n)を必要とします。それは本当です。

ただし、多くの場合、尾を引くだけで十分です。また、アレイを安価に作成する方法がない場合、サブアレイを安価に作成できても役に立たないことを考慮する必要があります。そして(巧妙なコンパイラー最適化がなければ)段階的に配列を安価に構築する方法はありません。


それはまったく真実ではありません。償却O(1)の配列に追加できます。
-DeadMG

10
@DeadMGはい、ただし不変の配列ではありません。
sepp2k

「部分的に共有する」-2つのコンスリストが同じサフィックスリストを指すことができ(これが必要な理由は間違いない)、コピーすることなくリストの先頭ではなく中点を別の関数に渡すことができると思うそれ(私はこれを何度もやった)
-Izkata

@Izkata OPは、リストではなく、部分的に配列を共有することについて話していました。また、あなたが説明している部分共有と呼ばれるものは聞いたことがありません。それはただ共有しています。
sepp2k

1
@Izkata OPは、「配列」という用語を正確に3回使用します。FP言語はリンクリストを使用し、他の言語は配列を使用します。配列は、リンクリストよりも部分共有の方が優れていると言い、一度は(リンクリストとして)配列も同様にパターンマッチできると言います。すべての場合において、彼は配列とリンクリストを対比しています(リンクリストよりも配列が主要なデータ構造としてより有用であるというポイントを作るために、FPでリンクリストが好まれる理由を疑問視します)。用語を同じ意味で使用できます。
sepp2k

4

リストは機能的なコードでかなり簡単に実装できると思います。

スキーム:

(define (cons x y)(lambda (m) (m x y)))

ハスケル:

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

配列はより難しく、実装するのがそれほどきれいではありません。それらを非常に高速にしたい場合は、低レベルで記述する必要があります。

また、リストでは配列よりも再帰がはるかに優れています。リストを再帰的に使用/生成した回数と配列のインデックスを作成した回数を考慮してください。


スキームバージョンをリンクリストの実装と呼ぶのは正確だとは言いません。関数以外を保存するために使用することはできません。また、配列は、組み込みのサポート(またはメモリチャンク)を持たない言語で実装するのが(実際には不可能です)ですが、リンクリストは、実装するのに構造体、クラス、レコード、または代数データ型のようなもののみを必要とします。それは関数型プログラミング言語に固有のものではありません。
sepp2k

@ sepp2k 「関数以外のものを保存するとはどういう意味ですか?
パブ

1
私が意味したのは、そのように定義されたリストは、関数ではないものを保存できないということでした。しかし、実際にはそうではありません。ダンノ、なぜそう思ったの。ごめんなさい
sepp2k

2

単一リンクリストは、最も単純な永続データ構造です。

永続的なデータ構造は、パフォーマンスの高い純粋に機能的なプログラミングに不可欠です。


1
これは単に繰り返しポイントがで作られたと説明しているようだトップの答えは 4年以上前に投稿されました
ブヨ

3
@gnat:一番上の答えは、永続的なデータ構造について言及していません。単一リンクリストは最も単純な永続的なデータ構造であるか、パフォーマンスの純粋な関数型プログラミングに不可欠です。一番上の答えとはまったく重複していません。
マイケルショー

2

Consノードは、ガベージコレクションされた言語を使用している場合にのみ簡単に使用できます。

短所ノードは、再帰呼び出しと不変値の関数型プログラミングスタイルと多くの点で一致します。したがって、メンタルプログラマモデルにうまく適合します。

そして歴史的な理由を忘れないでください。なぜ彼らはまだCons Nodesと呼ばれ、さらに悪いことにまだアクセサとしてcarとcdrを使用していますか?人々は教科書やコースからそれを学び、それを使います。

あなたは正しい、現実の世界では、アレイは使いやすく、メモリ空間の半分しか消費せず、キャッシュレベルのミスのためにパフォーマンスがはるかに優れています。それらを命令型言語で使用する理由はありません。


1

リンクリストは、次の理由で重要です。

3のような数を取得し、それをのような後継シーケンスに変換succ(succ(succ(zero))){succ=List node with some memory space}、それを、および{zero = end of list}で置き換えて使用すると、リンクリスト(長さ3)になります。

実際の重要な部分は、数字、置換、メモリ空間とゼロです。

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