66

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するメソッドを持つことができ、それを渡す必要はありません。stompMonkey

同様に、アーランやその他のアクター指向の設計では、これらの問題をかなりエレガントに解決できます。各アクターは独自のループを維持し、したがって独自の状態を維持するため、神オブジェクトは必要ありません。また、メッセージパッシングを使用すると、1つのオブジェクトのアクティビティが、コールスタックに戻るまで大量のデータを渡すことなく、他のオブジェクトの変更をトリガーできます。それでも、Haskellの俳優は憤慨していると言われていると聞きました。

4

2 に答える 2

31

答えは関数型リアクティブプログラミング(FRP)です。これは、コンポーネントの状態管理と時間依存の値という2つのコーディングスタイルのハイブリッドです。FRPは実際にはデザインパターンのファミリー全体であるため、より具体的にしたいと思います。Netwireをお勧めします。

基本的な考え方は非常に単純です。それぞれが独自のローカル状態を持つ、多くの小さな自己完結型のコンポーネントを作成します。このようなコンポーネントをクエリするたびに異なる回答が得られ、ローカル状態が更新される可能性があるため、これは実質的に時間依存の値と同等です。次に、これらのコンポーネントを組み合わせて実際のプログラムを作成します。

これは複雑で非効率に聞こえますが、実際には通常の関数の周りの非常に薄い層にすぎません。Netwireによって実装されたデザインパターンは、AFRP(Arrowized Functional Reactive Programming)に触発されています。それはおそらくそれ自身の名前(WFRP?)に値するほど十分に異なっています。チュートリアルを読むことをお勧めします。

いずれにせよ、小さなデモが続きます。ビルディングブロックはワイヤーです:

myWire :: WireP A B

これをコンポーネントと考えてください。これは、タイプAの時変値に依存する、タイプBの時変値です。たとえば、シミュレーターのパーティクルです。

particle :: WireP [Particle] Particle

これは、パーティクルのリスト(たとえば、現在存在するすべてのパーティクル)に依存し、それ自体がパーティクルです。事前定義されたワイヤー(簡略化されたタイプ)を使用してみましょう:

time :: WireP a Time

これは、 Time型(= Double )の時変値です。さて、それは時間そのものです(有線ネットワークが開始されたときから数えて0から始まります)。それは別の時変値に依存しないので、あなたはそれを好きなように与えることができます、それ故に多形入力タイプ。一定のワイヤー(時間とともに変化しない時間変化する値)もあります:

pure 15 :: Wire a Integer

-- or even:
15 :: Wire a Integer

2本のワイヤーを接続するには、単純にカテゴリ構成を使用します。

integral_ 3 . 15

これにより、3(積分定数)から始まる15倍のリアルタイム速度(時間の経過に伴う15の積分)のクロックが得られます。さまざまなクラスインスタンスのおかげで、ワイヤを組み合わせるのは非常に便利です。通常の演算子だけでなく、アプリケーションスタイルまたは矢印スタイルも使用できます。10から始まり、リアルタイム速度の2倍の時計が必要ですか?

10 + 2*time

(0、0)の速度で始まり、(0、0)で始まり、毎秒(2、1)で加速するパーティクルが必要ですか?

integral_ (0, 0) . integral_ (0, 0) . pure (2, 1)

ユーザーがスペースバーを押している間に統計を表示したいですか?

stats . keyDown Spacebar <|> "stats currently disabled"

これは、Netwireがあなたのためにできることのほんの一部です。

于 2013-03-18T01:47:46.607 に答える
1

私はこれが古いトピックであることを知っています。しかし、exercism.ioからRail Fence暗号演習を実装しようとしているときに、同じ問題に直面しています。Haskellでこのような一般的な問題があまり注目されていないのを見るのは非常に残念です。私は、FRPを学ぶために必要な状態を維持するのと同じくらい簡単なことをするのではありません。それで、私はグーグルを続けて、より簡単に見える解決策を見つけました-州のモナド:https ://en.wikibooks.org/wiki/Haskell/Understanding_monads/State

于 2017-12-28T17:32:47.673 に答える