関数型プログラミングでは、ほとんどすべてのデータ構造が不変であるため、状態を変更する必要がある場合、新しい構造が作成されます。これはより多くのメモリ使用量を意味しますか?オブジェクト指向プログラミングのパラダイムをよく知っているので、今は関数型プログラミングのパラダイムについて学ぼうとしています。すべてが不変であるという概念は私を混乱させます。不変の構造を使用するプログラムは、可変の構造を持つプログラムよりも多くのメモリを必要とするように思われます。私はこれを正しい方法で見ていますか?
関数型プログラミングでは、ほとんどすべてのデータ構造が不変であるため、状態を変更する必要がある場合、新しい構造が作成されます。これはより多くのメモリ使用量を意味しますか?オブジェクト指向プログラミングのパラダイムをよく知っているので、今は関数型プログラミングのパラダイムについて学ぼうとしています。すべてが不変であるという概念は私を混乱させます。不変の構造を使用するプログラムは、可変の構造を持つプログラムよりも多くのメモリを必要とするように思われます。私はこれを正しい方法で見ていますか?
回答:
関数型プログラミングでは、ほとんどすべてのデータ構造が不変であるため、状態を変更する必要がある場合、新しい構造が作成されます。これはより多くのメモリ使用量を意味しますか?
これは、データ構造、実行した正確な変更、および場合によってはオプティマイザーによって異なります。一例として、リストの先頭に追加することを考えてみましょう。
list2 = prepend(42, list1) // list2 is now a list that contains 42 followed
// by the elements of list1. list1 is unchanged
ここで、追加のメモリ要件は一定prepend
です。呼び出しの実行時コストも同様です。どうして?prepend
単純に42
、頭とlist1
尻尾を持つ新しいセルを作成するからです。これlist2
を実現するために、コピーしたり繰り返したりする必要はありません。つまり、保存42
に必要なメモリを除きlist2
、で使用されるのと同じメモリを再利用しlist1
ます。両方のリストは不変なので、この共有は完全に安全です。
同様に、バランスのとれたツリー構造で作業する場合、ほとんどの操作では、ツリーの1つのパスを共有できるため、対数的な追加スペースのみが必要です。
配列の場合、状況は少し異なります。そのため、多くのFP言語では、配列はそれほど一般的に使用されません。あなたのような何かを行う場合は、arr2 = map(f, arr1)
およびarr1
この行の後に再び使用されることはありません、スマートオプティマイザは、実際に変異するコードを作成することができますarr1
(プログラムの動作に影響を与えずに)代わりに、新しい配列を作成するのを。その場合、パフォーマンスはもちろん命令型言語のようになります。
単純な実装では、実際にこの問題が明らかになります。既存のデータ構造をその場で更新するのではなく、新しいデータ構造を作成する場合、オーバーヘッドが必要です。
異なる言語はこれに対処する異なる方法を持ち、それらのほとんどが使用するいくつかのトリックがあります。
1つの戦略はガベージコレクションです。新しい構造が作成された瞬間、またはその直後に、古い構造への参照が範囲外になり、GCアルゴリズムに応じて、ガベージコレクターがすぐにまたはすぐにそれを取得します。つまり、オーバーヘッドはまだありますが、一時的なものであり、データ量に比例して増加することはありません。
もう1つは、さまざまな種類のデータ構造の選択です。配列が命令型言語(通常std::vector
、C ++ などの動的再割り当てコンテナにラップされている)のリストのデータ構造である場合、関数型言語はリンクリストを好むことがよくあります。リンクリストを使用すると、プリペンド操作( 'cons')は既存のリストを新しいリストの末尾として再利用できるため、実際に割り当てられるのは新しいリストの先頭だけです。他の種類のデータ構造(セット、ツリー、名前など)にも同様の戦略が存在します。
そして、Haskellのような怠laな評価があります。アイデアは、作成するデータ構造がすぐに完全に作成されるわけではないということです。代わりに、「サンク」として保存されます(必要なときに値を構築するためのレシピと考えることができます)。値が必要な場合のみ、サンクは実際の値に展開されます。つまり、評価が必要になるまでメモリ割り当てを延期でき、その時点で、複数のサンクを1つのメモリ割り当てに結合できます。
私はClojureについて少しだけ知っていて、それは不変のデータ構造です。
Clojureは、不変のリスト、ベクター、セット、マップのセットを提供します。変更できないため、不変コレクションから何かを「追加」または「削除」することは、古いコレクションと同じように必要な変更を加えた新しいコレクションを作成することを意味します。永続性とは、「変更」後も古いバージョンのコレクションが引き続き使用可能であり、コレクションがほとんどの操作に対してパフォーマンスの保証を維持するプロパティを説明するために使用される用語です。具体的には、これは、完全なコピーを使用して新しいバージョンを作成できないことを意味します。これには、時間がかかるためです。必然的に、永続コレクションはリンクされたデータ構造を使用して実装されるため、新しいバージョンは以前のバージョンと構造を共有できます。
グラフィカルに、次のようなものを表すことができます。
(def my-list '(1 2 3))
+---+ +---+ +---+
| 1 | ---> | 2 | ---> | 3 |
+---+ +---+ +---+
(def new-list (conj my-list 0))
+-----------------------------+
+---+ | +---+ +---+ +---+ |
| 0 | --->| | 1 | ---> | 2 | ---> | 3 | |
+---+ | +---+ +---+ +---+ |
+-----------------------------+
他の回答で述べられたことに加えて、いわゆるユニークな型をサポートするCleanプログラミング言語に言及したいと思います。私はこの言語を知りませんが、ユニークな型が何らかの「破壊的な更新」をサポートすると思います。
つまり、状態を更新するセマンティクスは、関数を適用して古い値から新しい値を作成することですが、一意性制約により、コンパイラーは古い値が参照されないことを知っているため、データオブジェクトを内部で再利用できます新しい値が生成された後、プログラムでそれ以上。
詳細については、クリーンホームページやこのウィキペディアの記事をご覧ください。