次のすべてを備えたシステムを構築するにはどうすればよいですか:
Stateモナドはルール#2に違反しているように見えますが、モナドを介して組み込まれているため、明らかではありません。
どういうわけかレンズを使用する必要があると感じていますが、非関数型言語についてはほとんど書かれていません。
背景
演習として、既存のアプリケーションの1つをオブジェクト指向スタイルから機能スタイルに変換しています。私が最初にしようとしていることは、アプリケーションの内部コアをできるだけ多くすることです。
私が聞いたことの1つは、純粋に関数型の言語で「状態」を管理する方法です。これは、状態モナドによって行われると私が信じていることです。論理的には、純粋な関数を呼び出して、「世界がそのまま」の場合、関数が戻ると、変更された世界の状態が返されます。
たとえば、純粋に機能的な方法で「hello world」を実行する方法は、プログラムに画面の状態を渡し、「hello world」が印刷された画面の状態を受け取るようなものです。つまり、技術的には、純粋な関数を呼び出しているだけであり、副作用はありません。
これに基づいて、アプリケーションを調べました。1.最初に、すべてのアプリケーション状態を単一のグローバルオブジェクト(GameState)に入れます。2。次に、GameStateを不変にしました。変更することはできません。変更が必要な場合は、新しいものを作成する必要があります。これを行うには、オプションで変更された1つ以上のフィールドを取得するコピーコンストラクタを追加します。 3.各アプリケーションに、GameStateをパラメーターとして渡します。関数内では、実行する処理を行った後、新しいGameStateを作成して返します。
純粋な機能コアと、そのGameStateをアプリケーションのメインワークフローループにフィードする外側のループをどのように使用するか。
私の質問:
さて、私の問題は、GameStateに約15種類の不変オブジェクトがあることです。最下位レベルの関数の多くは、スコアを維持するなど、それらのオブジェクトの一部でのみ動作します。それでは、スコアを計算する関数があるとします。今日、GameStateはこの関数に渡されます。この関数は、新しいスコアで新しいGameStateを作成することによってスコアを変更します。
それについて何かが間違っているようです。関数はGameState全体を必要としません。必要なのは、Scoreオブジェクトだけです。スコアを渡し、スコアのみを返すように更新しました。
それは理にかなっているようだったので、私は他の機能をさらに進めました。一部の関数ではGameStateから2、3、または4つのパラメーターを渡す必要がありますが、パターンをアプリケーションの外側のコアまでずっと使用しているため、ますます多くのアプリケーション状態を渡しています。同様に、ワークフローループの先頭で、メソッドを呼び出し、メソッドを呼び出すメソッドを呼び出します。スコアが計算されるところまでずっと続きます。つまり、一番下の関数がスコアを計算しようとしているからといって、現在のスコアがこれらすべてのレイヤーに渡されます。
だから今私は時々数十のパラメーターを持つ関数を持っています。これらのパラメーターをオブジェクトに挿入してパラメーターの数を減らすことはできますが、そのクラスを、呼び出し時に単に構築されたオブジェクトではなく、状態のアプリケーション状態のマスターの場所にしたいと思います。複数のパラメータで、それらを解凍します。
だから今私が持っている問題は私の関数があまりにも深くネストされていることだと思います。これは小さな関数が欲しいという結果なので、関数が大きくなるとリファクタリングし、それを複数の小さな関数に分割します。しかし、これを行うとより深い階層が作成され、内部関数に渡されたものはすべて、外部関数がそれらのオブジェクトを直接操作していない場合でも、外部関数に渡される必要があります。
この問題を回避する方法に沿って単にGameStateを渡すように見えました。しかし、関数が必要とするよりも多くの情報を関数に渡すという元の問題に戻ります。
私はC#と話すことはできませんが、Haskellでは、最終的に状態全体を渡すことになります。これは明示的に行うことも、Stateモナドを使用して行うこともできます。関数が必要とするより多くの情報を受け取る関数の問題に対処するためにできることの1つは、Has型クラスを使用することです。 (詳しくない場合は、Haskellの型クラスはC#インターフェースに少し似ています。)状態の要素Eごとに、Eの値を返す関数getEを必要とする型クラスHasEを定義できます。状態モナドは、これらすべての型クラスのインスタンスを作成しました。次に、実際の関数では、Stateモナドを明示的に要求する代わりに、必要な要素のHas型クラスに属するモナドが必要です。これは、関数が使用しているモナドで実行できることを制限します。このアプローチの詳細については、Michael Snoymanの ReaderTデザインパターンの投稿 を参照してください。
渡されている状態をどのように定義しているかに応じて、おそらくC#でこのようなものを複製できます。あなたのようなものがあれば
public class MyState
{
public int MyInt {get; set; }
public string MyString {get; set; }
}
インターフェイスIHasMyInt
とIHasMyString
をそれぞれメソッドGetMyInt
とGetMyString
で定義できます。状態クラスは次のようになります。
public class MyState : IHasMyInt, IHasMyString
{
public int MyInt {get; set; }
public string MyString {get; set; }
public double MyDouble {get; set; }
public int GetMyInt ()
{
return MyInt;
}
public string GetMyString ()
{
return MyString;
}
public double GetMyDouble ()
{
return MyDouble;
}
}
次に、必要に応じて、メソッドにIHasMyInt、IHasMyString、またはMyState全体を要求できます。
次に、関数定義でwhere制約を使用して、状態オブジェクトを渡すことができますが、それはstringおよびintにのみ到達でき、doubleには到達できません。
public static T DoSomething<T>(T state) where T : IHasMyString, IHasMyInt
{
var s = state.GetMyString();
var i = state.GetMyInt();
return state;
}
良い解決策があるかどうかはわかりません。これは答えかもしれませんし、そうでないかもしれませんが、コメントには長すぎます。私は 似たようなこと をしていて、次のトリックが役に立ちました:
GameState
を階層的に分割すると、15ではなく3〜5個の小さなパーツが得られます。だから今私が持っている問題は私の関数があまりにも深くネストされていることだと思います。
そうは思いません。小さな関数へのリファクタリングは正しいですが、多分それらをよりよく再グループ化することができます。場合によっては、それが不可能である場合もあれば、問題を2回目(または3回目)で確認する必要がある場合もあります。
設計を変更可能な設計と比較します。書き換えによって悪化したものはありますか?もしそうなら、あなたが最初にしたのと同じ方法でそれらをより良くすることができませんか?
ReduxまたはElmと、それらがこの質問をどのように処理するかについて学ぶことはうまくいくと思います。
基本的に、状態全体とユーザーが実行したアクションを取り、新しい状態を返す1つの純粋な関数があります。
次に、その関数は他の純粋な関数を呼び出し、それぞれが特定の状態を処理します。アクションに応じて、これらの関数の多くは元の状態を変更せずに返すだけです。
詳細については、Google ElmアーキテクチャまたはRedux.js.orgをご覧ください。