関数型プログラミングは、同じオブジェクトが複数の場所から参照される状況をどのように処理しますか?


10

人々(このサイトでも)が日常的に関数型プログラミングのパラダイムを称賛していることを読んで聞いています。特に、C#、Java、C ++などの従来の命令型オブジェクト指向言語でも、プログラマーにこれを強制するHaskellのような純粋に関数型の言語だけでなく、このアプローチを提案します。

変異性と副作用が便利だと思うので、理解するのは難しいと思います。しかし、人々が現在副作用を非難し、可能な限りそれらを取り除くことが良い習慣であると考えると、私が有能なプログラマーになりたいなら、私はパラダイムのいくつかのより良い理解に向けて私の夢を始めなければならないと信じています...したがって、私のQ.

機能的パラダイムで問題を見つけたときの1つの場所は、オブジェクトが複数の場所から自然に参照されるときです。2つの例で説明します。

最初の例は、私が暇なときに作ろうとしているC#ゲームです。これはターンベースのWebゲームで、両方のプレイヤーに4つのモンスターのチームがあり、チームからモンスターを戦場に送ることができます。戦場では、相手のプレイヤーが送ったモンスターと対戦します。プレイヤーは戦場からモンスターを呼び戻し、それらを自分のチームの別のモンスターに置き換えることができます(ポケモンと同様に)。

この設定では、単一のモンスターを少なくとも2か所から自然に参照できます。プレイヤーのチームと2つの「アクティブな」モンスターを参照する戦場です。

ここで、1つのモンスターがヒットし、ヘルスポイントが20失われる状況を考えてみましょう。命令パラダイムの括弧内で、このモンスターのhealthフィールドを変更して、この変更を反映させます。これが私が現在行っていることです。ただし、これによりMonsterクラスが変更可能になり、関連する関数(メソッド)が不純になります。これは、現時点では悪い習慣と考えられています。

私は自分にこのゲームのコードを理想的な状態ではない状態にして、実際に将来のある時点でそれを完成させるという希望を持たせる許可を与えましたが、それがどのようにあるべきかを知り、理解したいと思います適切に書かれた。したがって、これが設計上の欠陥である場合、どのように修正すればよいですか?

機能的なスタイルでは、私が理解しているように、代わりにこのMonsterオブジェクトのコピーを作成し、この1つのフィールドを除いて古いオブジェクトと同じにします。suffer_hitこのメソッドは、古いオブジェクトを変更する代わりに、この新しいオブジェクトを返します。次に、同様にBattlefieldオブジェクトをコピーし、このモンスター以外のすべてのフィールドを同じに保ちます。

これには少なくとも2つの困難が伴います。

  1. 階層は、この単純化された単なるBattlefield->の例よりもはるかに深くすることができますMonster。1つを除くすべてのフィールドをこのようにコピーし、新しいオブジェクトをこの階層全体で返す必要があります。これはボイラープレートのコードです。関数型プログラミングはボイラープレートを減らすことになっているので、私は特に迷惑です。
  2. ただし、さらに深刻な問題は、データが同期なくなることです。フィールドのアクティブなモンスターは、体力が低下します。ただし、この同じモンスターは、その制御プレイヤーから参照されTeam、そうではありません。代わりに命令型のスタイルを採用した場合、データのすべての変更はコードの他のすべての場所から即座に表示され、このような場合には非常に便利ですが、これを実現する方法はまさに人々が言うことです命令型スタイルとは間違っています!
    • これでTeam、各攻撃の後に旅をすることで、この問題に対処できるようになります。これは余分な作業です。しかし、後でさらに多くの場所からモンスターが突然参照される可能性がある場合はどうなりますか?たとえば、モンスターがフィールド上にある必要のない別のモンスターに集中できるようにする能力が付いている場合はどうなりますか(実際にそのような能力を検討しています)。私はウィル確かに immediatellyそれぞれの攻撃の後にも焦点を当てたモンスターへの旅を取ることを覚えていますか?これは時限爆弾のようで、コードが複雑になると爆発するため、解決策はないと思います。

より良い解決策のアイデアは、同じ問題にぶつかったときの2番目の例から生まれます。学界では、Haskellで独自のデザインの言語の通訳を書くように言われました。(これは私がFPとは何かを理解し始めることを余儀なくされた方法でもあります)。クロージャーを実装しているときに問題が発生しました。もう一度同じスコープは現在、複数の場所から参照することができます。この範囲を保持する変数を通じおよびネストされたスコープの親スコープとして!明らかに、それを指している参照のいずれかを介してこのスコープに変更が加えられた場合、この変更は他のすべての参照からも見える必要があります。

私が付いた解決策は、各スコープにIDを割り当て、Stateモナド内のすべてのスコープの中央辞書を保持することでした。これで、変数はスコープ自体ではなく、バインドされたスコープのIDのみを保持し、ネストされたスコープは親スコープのIDも保持するようになります。

私のモンスターバトルゲームでも同じアプローチが試みられると思います...フィールドとチームはモンスターを参照していません。代わりに、中央のモンスターディクショナリに保存されているモンスターのIDを保持しています。

ただし、問題の解決策として躊躇せずにそれを受け入れることができないというこのアプローチの問題をもう一度見ることができます。

これもまた、ボイラープレートコードのソースです。これにより、1行が必ず3行になります。以前は単一フィールドの1行のインプレース変更でしたが、現在は(a)中央ディクショナリからオブジェクトを取得する(b)変更を加える(c)新しいオブジェクトを保存する中央の辞書に。また、参照の代わりにオブジェクトと中央の辞書のIDを保持すると、複雑さが増します。FPは複雑さと定型コードを減らすために宣伝されているので、これは私が間違っていることを示唆しています。

さらに深刻な2番目の問題についても書きました。このアプローチではメモリリークが発生します。到達できないオブジェクトは、通常、ガベージコレクションされます。ただし、到達可能なオブジェクトがこの特定のIDを参照していない場合でも、中央の辞書に保持されているオブジェクトをガベージコレクションすることはできません。また、理論的に注意深いプログラミングによりメモリリークを回避できます(各オブジェクトが不要になったら、手動で中央ディクショナリから削除することもできます)が、これはエラーが発生しやすく、FPはプログラムの正確さを高めるようにアドバタイズされます。正しい方法ではありません。

しかし、解決された問題であるように思われました。WeakHashMapこの問題の解決に使用できるJavaが提供しています。C#も同様の機能を提供します。ConditionalWeakTableただし、ドキュメントによると、コンパイラによる使用を目的としています。そしてHaskellにはSystem.Mem.Weakがあります。

そのような辞書を保存することは、この問題の正しい機能的な解決策ですか、それとも私が見落としているより単純なものがありますか?そのような辞書の数は簡単に増えてひどくなります。したがって、これらのディクショナリも不変であると想定されている場合、これは多くのパラメータを渡すことを意味します。または、それをサポートする言語では、モナド計算が行われます。これは、ディクショナリがモナドで保持されるためです(ただし、私は純粋に関数でそれを読んでいます)このディクショナリソリューションはほとんどすべてのコードをStateモナド内に配置しますが、これは正しいソリューションであるかどうかを疑います。

いくつか検討した後、もう1つ質問を追加します。このような辞書を作成することで何が得られるのでしょうか。多くの専門家によると、命令型プログラミングの問題点は、一部のオブジェクトの変更が他のコードに伝播することです。この問題を解決するために、オブジェクトは不変であると想定されています-このため、私が正しく理解していれば、オブジェクトに加えられた変更は他の場所には表示されないはずです。しかし、今は古くなったデータで動作する他のコードが気になるので、中央の辞書を発明して...もう一度、いくつかのコードの変更が他のコードに伝播するようにします!したがって、想定されるすべての欠点を伴う命令型のスタイルに戻りませんか?


6
これをある程度の観点から見ると、関数型の不変プログラムは主に、並行処理を伴うデータ処理状況向けです。 つまり、一連の方程式を通じて入力データを処理するプログラム、または出力結果を生成するプロセスです。不変性は、このシナリオでいくつかの理由で役立ちます。複数のスレッドによって読み取られている値は、その存続期間中に変更されないことが保証されています。
Robert Harvey

8
関数の不変性とゲームプログラミングについての汚い小さな秘密は、これら2つのものが互いに互換性がないことです。基本的に、静的で不動のデータ構造を使用して、動的で常に変化するシステムをモデル化しようとしています。
ロバートハーベイ

2
可変性vs不変性を宗教的な教義としてとらないでください。それぞれが他よりも優れている状況がありますが、不変性が常に優れているとは限りません。たとえば、不変のデータ型でGUIツールキットを書くことは絶対的な悪夢になります。
whatsisname

1
このC#固有の質問とその回答は、既存の不変オブジェクトのわずかに変更された(更新された)クローンを作成する必要性から生じるボイラープレートの問題をカバーしています。
rwong

2
重要な洞察は、このゲームのモンスターはエンティティと見なされるということです。また、各戦闘の結果(戦闘シーケンス番号、モンスターのエンティティID、戦闘前後のモンスターの状態からなる)は、特定の時点(またはタイムステップ)の状態と見なされます。したがって、プレーヤー(Team)は、(戦闘番号、モンスターエンティティID)タプルによって、戦闘の結果、つまりモンスターの状態を取得できます。
rwong

回答:


19

関数型プログラミングは、複数の場所から参照されるオブジェクトをどのように処理しますか?それはあなたのモデルを再訪することを勧めます!

説明するには...ネットワーク化されたゲームが時々どのように書かれるかを見てみましょう-ゲームの状態の中央の「ゴールデンソース」コピーと、その状態を更新し、次に他のクライアントにブロードキャストされる一連の受信クライアントイベント。

Factorioチームがこれをいくつかの状況でうまく機能させることで得た楽しさについて読むことができます。ここに彼らのモデルの短い概要があります:

マルチプレーヤーが機能する基本的な方法は、すべてのクライアントがゲームの状態をシミュレートし、プレーヤーの入力(入力アクションと呼ばれる)のみを送受信することです。サーバーの主な役割は、入力アクションをプロキシし、すべてのクライアントが同じティックで同じアクションを実行することを確認することです。

サーバーはアクションの実行時に調停を行う必要があるため、プレーヤーアクションは次のように移動します。プレーヤーアクション->ゲームクライアント->ネットワーク->サーバー->ネットワーク->ゲームクライアント。つまり、すべてのプレーヤーアクションは、ネットワークを往復して初めて実行されます。これにより、ゲームが本当に遅れるようになります。そのため、マルチプレイヤーの導入以来、レイテンシの非表示がゲームに追加されたメカニズムでした。レイテンシの非表示は、他のプレーヤーのアクションやサーバーのアービトラージを考慮せずに、プレーヤー入力をシミュレートすることで機能します。

Factorioにはゲームの状態があります。これは、マップ、プレーヤー、エンティティ、すべての完全な状態です。サーバーから受信したアクションに基づいて、すべてのクライアントで確定的にシミュレーションされます。これは神聖なものであり、サーバーや他のクライアントと異なる場合は、非同期が発生します。

ゲームステートの上にレイテンシステートがあります。これには、メイン状態の小さなサブセットが含まれています。レイテンシ状態は神聖ではなく、プレーヤーが実行した入力アクションに基づいて、ゲーム状態が将来どのように見えるかを表すだけです。

重要なことは、各オブジェクトの状態はタイムラインの特定のティックで不変であることです。グローバルマルチプレイヤー状態にあるすべてのものは、最終的に決定論的現実に収束しなければなりません。

そして、それがあなたの質問の鍵になるかもしれません。各エンティティの状態は特定のティックに対して不変であり、新しいインスタンスを生成する遷移イベントを追跡します。

考えてみれば、サーバーからの着信イベントキューは、そのイベントを適用できるように、エンティティの中央ディレクトリにアクセスできる必要があります。

結局のところ、複雑にしたくない単純な1行のミューテーターメソッドは、時間を正確にモデル化していないため、単純です。結局のところ、処理ループの途中でヘルスが変化する可能性がある場合、このティックの以前のエンティティには古い値が表示され、後のエンティティには変更された値が表示されます。これを注意深く管理することは、少なくとも現在の状態(不変)と次の状態(作成中)を区別することを意味します。

したがって、大まかなガイドとして、モンスターの状態を、たとえば場所/速度/物理、健康/損傷、資産などに関連するいくつかの小さなオブジェクトに分割することを検討してください。発生する可能性のある各変異を説明するイベントを作成し、メインループを次のように実行します。

  1. 入力を処理し、対応するイベントを生成する
  2. 内部イベントを生成します(オブジェクトの衝突などによる)
  3. 現在の不変のモンスターにイベントを適用し、次のティックのために新しいモンスターを生成します。ほとんどの場合、変更されていない古い状態を可能な限りコピーしますが、必要に応じて新しい状態オブジェクトを作成します。
  4. レンダリングし、次のティックのために繰り返します。

またはそのようなもの。「これをどのように配布するのですか?」物事がどこに住んでいて、どのように進化するべきかについて混乱しているときに理解を深めるために、一般的には非常に良い精神的な練習です。

@ AaronM.Eshbachからのメモに感謝します。これは、イベントソーシングCQRSパターンと同様の問題ドメインであり、分散システムでの状態の変化を、一連の不変のイベントとしてモデル化しています。この場合、クエリ/ビューシステムからミューテーターコマンドの処理を(名前が示すように)分離することで、複雑なデータベースアプリをクリーンアップしようとしている可能性があります。もちろんより複雑ですが、より柔軟です。


2
追加のリファレンスについては、「イベントソーシングCQRS」を参照してください。これは同様の問題領域です。分散システムでの状態の変化を、一連の不変のイベントとしてモデリングします。
アーロンM.エシュバッハ

@ AaronM.Eshbachあれだ!回答にコメント/引用を含めてもよろしいですか?それはそれをより権威的に聞こえるようにします。ありがとう!
SusanW

もちろん、しないでください。
アーロンM.エシュバッハ

3

あなたはまだ命令キャンプに半分残っています。一度に1つのオブジェクトについて考えるのではなく、プレイやイベントの履歴の観点からゲームを考えます

p1 - send m1 to battlefield
p2 - send m2 to battlefield
m1 - attacks m2 (2 dam)
m2 - attacks m1 (10 dam)
p1 - retreats m1

アクションをチェーンして不変の状態オブジェクトを生成することにより、任意の時点でのゲームの状態を計算できます。各プレイは、状態オブジェクトを取り、新しい状態オブジェクトを返す関数です

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