Haskellでかなり大規模なシミュレーションを構築していると仮定します。シミュレーションの進行に伴って属性が更新されるエンティティには、さまざまな種類があります。例として、エンティティがサル、ゾウ、クマなどと呼ばれているとします。
これらのエンティティの状態を維持するための好ましい方法は何ですか?
私が考えた最初のそして最も明白なアプローチはこれでした:
mainLoop :: [Monkey] -> [Elephant] -> [Bear] -> String
mainLoop monkeys elephants bears =
let monkeys' = updateMonkeys monkeys
elephants' = updateElephants elephants
bears' = updateBears bears
in
if shouldExit monkeys elephants bears then "Done" else
mainLoop monkeys' elephants' bears'
mainLoop
関数のシグネチャで各タイプのエンティティが明示的に言及されているのは、すでに醜いです。たとえば、20種類のエンティティがある場合、それがどのように絶対にひどくなるかを想像することができます。(20は、複雑なシミュレーションには不合理ではありません。)したがって、これは受け入れられないアプローチだと思います。しかし、その節約の恩恵は、のような関数がそれらの機能updateMonkeys
において非常に明示的であるということです。それらはサルのリストを取得し、新しいものを返します。
したがって、次の考えは、すべてを1つのビッグデータ構造にまとめて、すべての状態を保持し、次の署名をクリーンアップすることですmainLoop
。
mainLoop :: GameState -> String
mainLoop gs0 =
let gs1 = updateMonkeys gs0
gs2 = updateElephants gs1
gs3 = updateBears gs2
in
if shouldExit gs0 then "Done" else
mainLoop gs3
GameState
州のモナドで締めくくり、updateMonkeys
などで呼び出すことを提案する人もいdo
ます。それはいいです。むしろ、関数合成でクリーンアップすることを提案する人もいます。また大丈夫だと思います。(ところで、私はHaskellの初心者なので、これについては間違っているかもしれません。)
しかし、問題は、のような関数updateMonkeys
が型アノテーションから有用な情報を提供しないことです。あなたは彼らが何をしているのか本当に確信が持てません。確かにupdateMonkeys
、説明的な名前ですが、それは少し慰めです。神オブジェクトを渡して「グローバルな状態を更新してください」と言うと、命令型の世界に戻ったような気がします。それは別の名前のグローバル変数のように感じます。あなたはグローバル状態に何かをする関数を持っていて、それを呼び出し、そしてあなたは最高のものを望んでいます。(命令型プログラムのグローバル変数に存在する並行性の問題はまだ回避できると思います。しかし、グローバル変数の問題は並行性だけではありません。)
さらなる問題はこれです:オブジェクトが相互作用する必要があると仮定します。たとえば、次のような関数があります。
stomp :: Elephant -> Monkey -> (Elephant, Monkey)
stomp elephant monkey =
(elongateEvilGrin elephant, decrementHealth monkey)
これが呼び出されたとしupdateElephants
ましょう。ここで、象がサルの踏み鳴らされている範囲にいるかどうかを確認します。このシナリオでは、どのようにして変更をサルとゾウの両方にエレガントに伝播しますか?2番目の例でupdateElephants
は、神オブジェクトを取得して返すため、両方の変更に影響を与える可能性があります。しかし、これは水をさらに濁らせ、私の主張を補強します。神オブジェクトを使用すると、事実上、グローバル変数を変更するだけです。また、神オブジェクトを使用していない場合、これらのタイプの変更をどのように伝播するかはわかりません。
何をすべきか?確かに多くのプログラムが複雑な状態を管理する必要があるので、この問題にはいくつかのよく知られたアプローチがあると思います。
比較のために、OOPの世界で問題を解決する方法を次に示します。Monkey
、、Elephant
などのオブジェクトがあります。私はおそらく、すべての生きている動物のセットでルックアップを行うためのクラスメソッドを持っているでしょう。たぶん、場所やIDなどで検索できます。ルックアップ関数の基礎となるデータ構造のおかげで、それらはヒープに割り当てられたままになります。(私はGCまたは参照カウントを想定しています。)それらのメンバー変数は常に変化します。どのクラスのどのメソッドでも、他のクラスの生きている動物を突然変異させることができます。たとえば、渡されたオブジェクトのヘルスをデクリメントElephant
するメソッドを持つことができ、それを渡す必要はありません。stomp
Monkey
同様に、アーランやその他のアクター指向の設計では、これらの問題をかなりエレガントに解決できます。各アクターは独自のループを維持し、したがって独自の状態を維持するため、神オブジェクトは必要ありません。また、メッセージパッシングを使用すると、1つのオブジェクトのアクティビティが、コールスタックに戻るまで大量のデータを渡すことなく、他のオブジェクトの変更をトリガーできます。それでも、Haskellの俳優は憤慨していると言われていると聞きました。