FP(具体的にはF#ですが、他のFPも問題ありません)で不変データを処理することを理解し、ステートフル思考(OOPスタイル)の古い習慣を打破しようとしています。選択した質問の回答 here OOPのステートフル表現で解決される問題の周辺の記述について、FP(例:プロデューサー&コンシューマーのキュー)。ご意見やリンクはありますか?よろしくお願いします。
編集:質問をもう少し明確にするために、不変構造(例:キュー)を複数のスレッド(例:プロデューサーとコンシューマー)で同時に共有する方法FPで
そのように表現されることもありますが、関数型プログラミング¹はステートフルな計算を妨げません。それが行うことは、プログラマに状態を明示的にさせることです。
たとえば、命令キュー(ある疑似言語)を使用するプログラムの基本構造を見てみましょう。
_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つの特定のノードによって管理されます。コンシューマーはそのノードにメッセージを送信して要素を取得します。プロデューサーはそのノードにメッセージを送信して、要素を追加します。
_main_loop(q) =
consumer->consume(q->take()) || q->add(producer->produce());
main_loop(q)
_
同時実行を正しく行う1つの「工業化された」言語は Erlang です。 Erlangの学習は、間違いなく並行プログラミングに関するEnlightenment⁴への道です。
¹ この用語にはいくつかの意味があります。ここでは、副作用のないプログラミングを意味していると思いますが、それも私が使用している意味です。
² 暗黙的な状態でのプログラミングは、命令型プログラミングです。オブジェクトの向きは完全に直交する問題です。
³ 炎症性ですが、私はそれを意味します。共有メモリを使用するスレッドは、並行プログラミングのアセンブリ言語です。メッセージパッシングは非常に理解しやすく、同時実行性を導入するとすぐに副作用の欠如が明らかになります。
⁴ そしてこれはErlangのファンではない人から来ていますが、他の理由があります。
FP言語のステートフルな動作は、前の状態から新しい状態への変換として実装されます。たとえば、エンキューは、キューと値から新しいキューへの変換であり、値はエンキュー。デキューは、キューから値への変換であり、値が削除された新しいキューです。モナドのような構造は、この状態変換(およびその他の計算結果)を有用な方法で抽象化するために考案されました
... OOPのステートフル表現によって解決される問題FPの不変のものを使用する場合:生産者と消費者のキュー)
あなたの質問は、いわゆる「XY問題」です。具体的には、引用する概念(プロデューサーとコンシューマーのキュー)は実際にはsolutionであり、説明する「問題」ではありません。本質的に不純なものの純粋に機能的な実装を求めているため、これは困難をもたらします。だから私の答えは質問から始まります:あなたが解決しようとしている問題は何ですか?
複数のプロデューサーが結果を単一の共有コンシューマーに送信する方法はたくさんあります。おそらく、F#で最も明白な解決策は、コンシューマーをエージェント(別名MailboxProcessor
)にし、プロデューサーにその結果をコンシューマーエージェントにPost
させることです。これは内部的にキューを使用し、純粋ではありません(F#でメッセージを送信することは、制御されていない副作用であり、不純物です)。
ただし、根本的な問題は、並列プログラミングのスキャッター/ギャザーパターンのようなものである可能性が高いです。この問題を解決するには、入力値の配列を作成してからArray.Parallel.map
それらの上に、シリアルを使用して結果を収集しますArray.reduce
。または、PSeq
モジュールの関数を使用して、シーケンスの要素を並列処理することもできます。
また、ステートフルシンキングには何の問題もないことも強調しておきます。純度には利点がありますが、万能薬ではないので、その欠点にも注意する必要があります。実際、これがまさにF#が純粋な関数型言語ではない理由です。したがって、不純物が望ましい場合は不純物を使用できます。
Clojureには非常によく考えられた状態とアイデンティティの概念があり、これは並行性と密接に関連しています。不変性は重要な役割を果たし、Clojureのすべての値は不変であり、参照を通じてアクセスできます。参照は単なるポインタではありません。それらは値へのアクセスを管理し、セマンティクスが異なる複数のタイプがあります。参照は、新しい(不変の)値を指すように変更でき、そのような変更はアトミックであることが保証されています。ただし、変更後、少なくとも参照に再度アクセスするまで、他のすべてのスレッドは元の値で機能します。
Clojureでの状態とIDに関する優れた記事 を読むことを強くお勧めします。詳細については、私が説明したよりもはるかに詳しく説明しています。