人々(このサイトでも)が日常的に関数型プログラミングのパラダイムを称賛していることを読んで聞いています。特に、C#、Java、C ++などの従来の命令型オブジェクト指向言語でも、プログラマーにこれを強制するHaskellのような純粋に関数型の言語だけでなく、このアプローチを提案します。
変異性と副作用が便利だと思うので、理解するのは難しいと思います。しかし、人々が現在副作用を非難し、可能な限りそれらを取り除くことが良い習慣であると考えると、私が有能なプログラマーになりたいなら、私はパラダイムのいくつかのより良い理解に向けて私の夢を始めなければならないと信じています...したがって、私のQ.
機能的パラダイムで問題を見つけたときの1つの場所は、オブジェクトが複数の場所から自然に参照されるときです。2つの例で説明します。
最初の例は、私が暇なときに作ろうとしているC#ゲームです。これはターンベースのWebゲームで、両方のプレイヤーに4つのモンスターのチームがあり、チームからモンスターを戦場に送ることができます。戦場では、相手のプレイヤーが送ったモンスターと対戦します。プレイヤーは戦場からモンスターを呼び戻し、それらを自分のチームの別のモンスターに置き換えることができます(ポケモンと同様に)。
この設定では、単一のモンスターを少なくとも2か所から自然に参照できます。プレイヤーのチームと2つの「アクティブな」モンスターを参照する戦場です。
ここで、1つのモンスターがヒットし、ヘルスポイントが20失われる状況を考えてみましょう。命令パラダイムの括弧内で、このモンスターのhealth
フィールドを変更して、この変更を反映させます。これが私が現在行っていることです。ただし、これによりMonster
クラスが変更可能になり、関連する関数(メソッド)が不純になります。これは、現時点では悪い習慣と考えられています。
私は自分にこのゲームのコードを理想的な状態ではない状態にして、実際に将来のある時点でそれを完成させるという希望を持たせる許可を与えましたが、それがどのようにあるべきかを知り、理解したいと思います適切に書かれた。したがって、これが設計上の欠陥である場合、どのように修正すればよいですか?
機能的なスタイルでは、私が理解しているように、代わりにこのMonster
オブジェクトのコピーを作成し、この1つのフィールドを除いて古いオブジェクトと同じにします。suffer_hit
このメソッドは、古いオブジェクトを変更する代わりに、この新しいオブジェクトを返します。次に、同様にBattlefield
オブジェクトをコピーし、このモンスター以外のすべてのフィールドを同じに保ちます。
これには少なくとも2つの困難が伴います。
- 階層は、この単純化された単なる
Battlefield
->の例よりもはるかに深くすることができますMonster
。1つを除くすべてのフィールドをこのようにコピーし、新しいオブジェクトをこの階層全体で返す必要があります。これはボイラープレートのコードです。関数型プログラミングはボイラープレートを減らすことになっているので、私は特に迷惑です。 - ただし、さらに深刻な問題は、データが同期されなくなることです。フィールドのアクティブなモンスターは、体力が低下します。ただし、この同じモンスターは、その制御プレイヤーから参照され
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つ質問を追加します。このような辞書を作成することで何が得られるのでしょうか。多くの専門家によると、命令型プログラミングの問題点は、一部のオブジェクトの変更が他のコードに伝播することです。この問題を解決するために、オブジェクトは不変であると想定されています-このため、私が正しく理解していれば、オブジェクトに加えられた変更は他の場所には表示されないはずです。しかし、今は古くなったデータで動作する他のコードが気になるので、中央の辞書を発明して...もう一度、いくつかのコードの変更が他のコードに伝播するようにします!したがって、想定されるすべての欠点を伴う命令型のスタイルに戻りませんか?
Team
)は、(戦闘番号、モンスターエンティティID)タプルによって、戦闘の結果、つまりモンスターの状態を取得できます。