関数型プログラミング—不変性


12

私はFPで不変データを扱うことを理解しようとしています(特にF#ですが、他のFPも大丈夫です)、ステートフル思考(OOPスタイル)の古い習慣を破ります。ここでの質問に対する選択された回答の一部は、OOPのステートフルな表現とFPの不変な表現(たとえば、プロデューサーとコンシューマーのキュー)によって解決される問題に関する記述の検索を繰り返しました。何か考えやリンクは大歓迎ですか?前もって感謝します。

編集:質問をもう少し明確にするために、FPで複数のスレッド(例:プロデューサーとコンシューマー)で不変の構造(例:キュー)を同時に共有する方法


同時実行性の問題を処理する1つの方法は、毎回キューのコピーを作成することです(多少高価ですが機能します)。
ジョブ

infoq.com/presentations/Functional-Data-Structures-in-Scalaこのスピーチは洞察力に富んでいることがわかります。
deadalnix

回答:


19

関数型プログラミング¹は、そのように表現されることもありますが、ステートフルな計算を妨げません。それはプログラマに状態を明示的にさせることです。

たとえば、命令型キューを使用して(一部の疑似言語で)プログラムの基本構造を見てみましょう。

q := Queue.new();
while (true) {
    if (Queue.is_empty(q)) {
        Queue.add(q, producer());
    } else {
        consumer(Queue.take(q));
    }
}

機能キューデータ構造を持つ対応する構造(一度に1つの違いに対処するために、命令型言語で)は次のようになります。

q := Queue.empty;
while (true) {
    if (q = Queue.empty) {
        q := Queue.add(q, producer());
    } else {
        (tail, element) := Queue.take(q);
        consumer(element);
        q := tail;
    }
}

キューは不変であるため、オブジェクト自体は変更されません。この擬似コードでqは、それ自体が変数です。割り当てq := Queue.add(…)q := tail行い、別のオブジェクトを指すようにします。キュー関数のインターフェイスが変更されました。それぞれが、操作の結果である新しいキューオブジェクトを返す必要があります。

純粋に機能的な言語、つまり副作用のない言語では、すべての状態を明示的にする必要があります。プロデューサーとコンシューマーはおそらく何かをしているので、それらの状態もここの呼び出し元のインターフェースになければなりません。

main_loop(q, other_state) {
    if (q = Queue.empty) {
        let (new_state, element) = producer(other_state);
        main_loop(Queue.add(q, element), new_state);
    } else {
        let (tail, element) = Queue.take(q);
        let new_state = consumer(other_state, element);
        main_loop(tail, new_state);
    }
}
main_loop(Queue.empty, initial_state)

どのようにしてすべての状態が明示的に管理されるかに注意してください。キュー操作関数は、キューを入力として受け取り、出力として新しいキューを生成します。プロデューサーとコンシューマーも同様に状態を渡します。

並行プログラミングはとてもうまく合わないの内側に関数型プログラミング、それは非常によく適合周りの関数型プログラミング。アイデアは、多数の個別の計算ノードを実行し、それらにメッセージを交換させることです。各ノードは機能プログラムを実行し、メッセージの送受信に応じて状態が変化します。

例を続けると、キューが1つしかないため、1つの特定のノードによって管理されます。消費者はそのノードにメッセージを送信して要素を取得します。プロデューサーはそのノードにメッセージを送信して要素を追加します。

main_loop(q) =
    consumer->consume(q->take()) || q->add(producer->produce());
    main_loop(q)

並行処理を正しく行う「工業化された」言語はErlangです。Erlangを学ぶことは、間違いなく並行プログラミングについての啓発への道です。

誰もが副作用のない言語に今すぐ切り替えます!

¹ この用語にはいくつかの意味があります。ここで私はあなたが副作用なしでプログラミングを意味するためにそれを使用していると思う、そしてそれは私も使用している意味だ。
² 暗黙的な状態でのプログラミングは、命令型プログラミングです。オブジェクトの向きは完全に直交する問題です。
³ 炎症性、私は知っていますが、私はそれを意味します。共有メモリを持つスレッドは、並行プログラミングのアセンブリ言語です。メッセージの受け渡しは非常に理解しやすく、同時実行性を導入するとすぐに副作用の欠如が明らかになります。
これは、Erlangのファンではないが、他の理由からだ。


2
+1より完全な答え。ただし、Erlangは純粋なFP言語ではないことをすることができると思います。
ラインヘンリッヒス

1
@Rein Henrichs:確かに。実際、現在存在するすべての主流言語の中で、Erlangはオブジェクト指向を最も忠実に実装している言語です。
ヨルグWミッターグ

2
@ヨルグ合意。繰り返しますが、純粋なFPとOOは直交しているとquiすることもできます。
ラインヘンリヒズ

したがって、並行ソフトウェアで不変キューを実装するには、ノード間でメッセージを送受信する必要があります。保留中のメッセージはどこに保存されますか?
ムーヴィシエル

@mouvicielキュー要素は、ノードの着信メッセージキューに格納されます。このメッセージキュー機能は、分散インフラストラクチャの基本機能です。分散システムではなくローカルの並行性にうまく機能する代替インフラストラクチャの設計は、受信者の準備ができるまで送信者をブロックすることです。これがすべてを説明しているわけではないことを理解しています。これを完全に説明するには、並行プログラミングに関する本の1〜2章が必要です。
ジル 'SO-悪であるのをやめる'

4

FP言語のステートフルな動作は、以前の状態から新しい状態への変換として実装されます。たとえば、エンキューは、キューからの変換と、値がエンキューされた新しいキューへの変換です。デキューは、キューから値への変換、および値が削除された新しいキューになります。モナドのような構造は、この状態変換(およびその他の計算結果)を便利な方法で抽象化するために考案されました


3
すべての追加/削除操作の新しいキューの場合、2つ(またはそれ以上)の非同期操作(スレッド)はどのようにキューを共有しますか?キューの新規作成を抽象化するパターンですか?
venkram

並行性はまったく別の質問です。コメントで十分な回答を提供できません。
ラインヘンリヒス

2
@Rein Henrichs:「コメントで十分な答えを提供できない」。これは通常、コメント関連の問題に対処するために回答を更新する必要があることを意味します。
-S.ロット

並行性も単項式である可能性があります。haskellsControl.Concurrency.STMを参照してください。
代替

1
この場合の@ S.Lottは、OPが新しい質問をする必要があることを意味します。並行性は、この質問に対するOTです。これは、不変のデータ構造に関するものです。
ラインヘンリヒス

2

... OOPのステートフル表現とFPの不変表現で解決される問題(例:プロデューサーとコンシューマーのキュー)

あなたの質問は、「XY問題」と呼ばれるものです。具体的には、引用する概念(プロデューサーとコンシューマーを含むキュー)は実際にはソリューションであり、説明する「問題」ではありません。本質的に不純な何かの純粋に機能的な実装を求めているため、これは困難をもたらします。だから私の答えは質問から始まります:あなたが解決しようとしている問題は何ですか?

複数のプロデューサーが結果を単一の共有コンシューマーに送信する方法は多数あります。おそらく、F#で最も明らかな解決策は、消費者をエージェント(別名MailboxProcessor)にし、生産Post者に消費者エージェントに結果を提供することです。これは内部的にキューを使用し、純粋ではありません(F#でのメッセージの送信は制御されない副作用であり、不純物です)。

ただし、根本的な問題は、並列プログラミングのスキャッターギャザーパターンに似ている可能性が高いです。この問題を解決するには、入力値の配列を作成し、Array.Parallel.mapそれらの上にシリアルを使用して結果を収集しますArray.reduce。または、PSeqモジュールの関数を使用して、シーケンスの要素を並列処理することもできます。

また、ステートフルな思考には何の問題もないことを強調する必要があります。純度には利点がありますが、万能薬ではありません。その欠点も認識しておく必要があります。実際、これがまさにF#が純粋な関数型言語ではない理由です。したがって、必要に応じて不純物を使用できます。


1

Clojureには、並行性に密接に関連する状態とアイデンティティの概念が非常によく考えられています。不変性は重要な役割を果たし、Clojureのすべての値は不変であり、参照を通じてアクセスできます。参照は単なるポインタ以上のものです。値へのアクセスを管理し、セマンティクスが異なる複数のタイプがあります。参照は、新しい(不変の)値を指すように変更でき、そのような変更はアトミックであることが保証されます。ただし、変更後も、少なくとも参照に再度アクセスするまで、他のすべてのスレッドは元の値で動作します。

Clojureの状態とアイデンティティに関する優れた記事を読むことを強くお勧めします。詳細を説明するよりもはるかに優れています。

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