15

Haskell で MUD サーバーを作成しています (MUD = Multi User Dungeon: 基本的に、マルチユーザー テキスト アドベンチャー/ロールプレイング ゲーム)。ゲーム世界のデータ/状態は、約 15IntMapの異なる で表されます。私のモナド トランスフォーマー スタックは次のようになります: ReaderT MudData IO、ここで、MudData型は s を含むレコード型でIntMap、それぞれに独自のものがありますTVar(同時実行のために STM を使用しています):

data MudData = MudData { _armorTblTVar    :: TVar (IntMap Armor)
                       , _clothingTblTVar :: TVar (IntMap Clothing)
                       , _coinsTblTVar    :: TVar (IntMap Coins)

...等々。(私はレンズを使用しているため、アンダースコアです。)

一部の機能には特定IntMapの が必要ですが、他の機能には他の機能が必要です。したがって、それぞれIntMapを独自に持つことで、TVar粒度が提供されます。

ただし、コードにパターンが現れました。TVarプレーヤー コマンドを処理する関数では、STM モナド内の sに読み取る (場合によっては後で書き込む) 必要があります。したがって、これらの関数は、whereブロックで定義された STM ヘルパーを持つことになります。readTVarほとんどのコマンドは少数の にアクセスする必要があるため、これらの STM ヘルパーにはかなりの数の操作が含まれていることがよくありますIntMap。さらに、特定のコマンドの関数は、 の一部またはすべてを必要とする多くの純粋なヘルパー関数を呼び出す場合がありますIntMap。したがって、これらの純粋なヘルパー関数は、多くの引数 (場合によっては 10 以上) を取ることになります。

そのため、私のコードは、readTVar多数の引数をとる多くの式と関数で「散らかって」います。ここに私の質問があります: これはコードのにおいですか? コードをよりエレガントにする抽象化が欠けていますか? データ/コードを構造化するためのより理想的な方法はありますか?

ありがとう!

4

2 に答える 2

22

この問題の解決策は、純粋なヘルパー関数を変更することです。それらが純粋であることを本当に望んでいるわけではありません。特定のデータを読み取るかどうかに関係なく、単一の副作用を漏らしたいのです。

衣服とコインのみを使用する純粋な関数があるとします。

moreVanityThanWealth :: IntMap Clothing -> IntMap Coins -> Bool
moreVanityThanWealth clothing coins = ...

通常、関数が衣服やコインなどにのみ関心があることを知っておくと便利ですが、あなたの場合、この知識は無関係であり、頭痛の種になっています。この詳細は意図的に忘れてしまいます。mb14 の提案に従えばMudData'、次のようなピュア全体をヘルパー関数に渡します。

data MudData' = MudData' { _armorTbl    :: IntMap Armor
                         , _clothingTbl :: IntMap Clothing
                         , _coinsTbl    :: IntMap Coins

moreVanityThanWealth :: MudData' -> Bool
moreVanityThanWealth md =
    let clothing = _clothingTbl md
        coins    = _coinsTbl    md
    in  ...

MudDataMudData'ほぼ同じです。それらの 1 つはそのフィールドをTVars でラップし、もう 1 つはラップしません。フィールドをラップするためMudDataの追加の型パラメーター ( kind の) を受け取るように変更できます。レンズと密接に関連していますが、ライブラリのサポートはあまりありません。このパターンをModelと呼びます。* -> *MudData(* -> *) -> *

data MudData f = MudData { _armorTbl    :: f (IntMap Armor)
                         , _clothingTbl :: f (IntMap Clothing)
                         , _coinsTbl    :: f (IntMap Coins)

MudDataでオリジナルを復元できMudData TVarます。Identityフィールドを, でラップすることにより、純粋なバージョンを再作成できますnewtype Identity a = Identity {runIdentity :: a}。に関してはMudData Identity、関数は次のように記述されます。

moreVanityThanWealth :: MudData Identity -> Bool
moreVanityThanWealth md =
    let clothing = runIdentity . _clothingTbl $ md
        coins    = runIdentity . _coinsTbl    $ md
    in  ...

のどの部分を使用したかを忘れることに成功しましたMudDataが、必要なロックの粒度がありません。副作用として、忘れていたものを正確に回復する必要があります。STMヘルパーのバージョンを書くと、次のようになります

moreVanityThanWealth :: MudData TVar -> STM Bool
moreVanityThanWealth md =
    do
        clothing <- readTVar . _clothingTbl $ md
        coins    <- readTVar . _coinsTbl    $ md
        return ...

のこのSTMバージョンMudData TVarは、先ほど書いた の純粋なバージョンとほぼ同じですMudData Identity。それらは、参照のタイプ ( TVarvs. Identity)、参照から値を取得するために使用する関数 ( readTVarvs runIdentity)、および結果がどのように返されるか (STMまたはプレーン値として) だけが異なります。同じ関数を使用して両方を提供できるとよいでしょう。2 つの機能の共通点を抽出します。そのために、ある型の参照を読み取ることができる s の型クラスMonadReadRef r mを導入します。は参照の型、 は参照から値を取得する関数、は結果を返す方法です。以下は、MonadrreadRefmMonadReadRefMonadRefref-fdからのクラス。

{-# LANGUAGE FunctionalDependencies #-}

class Monad m => MonadReadRef r m | m -> r where
    readRef :: r a -> m a

コードがすべての でパラメータ化されている限り、コードMonadReadRef r mは純粋です。MonadReadRefこれは、 に保持されている通常の値の次のインスタンスで実行することで確認できますIdentity。インidreadRef = idと同じreturn . runIdentityです。

instance MonadReadRef Identity Identity where
    readRef = id

moreVanityThanWealthという形で書き直しますMonadReadRef

moreVanityThanWealth :: MonadReadRef r m => MudData r -> m Bool
moreVanityThanWealth md =
    do
        clothing <- readRef . _clothingTbl $ md
        coins    <- readRef . _coinsTbl    $ md
        return ...

にs のMonadReadRefインスタンスを追加すると、これらの「純粋な」計算を で使用できますが、 s が読み込まれたことによる副作用が漏れます。TVarSTMSTMTVar

instance MonadReadRef TVar STM where
    readRef = readTVar
于 2015-03-07T19:56:40.630 に答える
16

はい、これは明らかにコードを複雑にし、多くのボイラープレートの詳細で重要なコードを混乱させます。また、引数が 4 つを超える関数は、問題の兆候です。

質問したいのですが、別々の sを持つことで本当に何か得られるのでしょうか? TVar時期尚早の最適化の場合ではありませんか? データ構造を複数の個別の に分割するなどの設計上の決定を下す前に、TVar必ずいくつかの測定を行います (基準を参照)。予想される同時スレッド数とデータ更新の頻度をモデル化するサンプル テストを作成し、複数TVarの 対 単一の 対IORef.

次の点に注意してください。

  • トランザクション内の共通ロックをめぐって複数のスレッドが競合している場合STM、トランザクションが正常に完了する前に、トランザクションが数回再起動される可能性があります。そのため、状況によっては、複数のロックを使用すると実際に事態が悪化する可能性があります。
  • 同期する必要があるデータ構造が最終的に 1 つしかない場合は、IORef代わりに単一のデータ構造を使用することを検討してください。そのアトミック操作は非常に高速であり、単一の中央ロックを持つことを補うことができます.
  • STMHaskell では、純粋な関数がアトミックまたはIORefトランザクションを長時間ブロックすることは驚くほど困難です。その理由は怠惰です。そのようなトランザクション内でサンクを作成するだけでよく、それらを評価する必要はありません。これは、特に単一の atom の場合に当てはまりますIORef。サンクは、そのようなトランザクションの外部で評価されます (それらを検査するスレッドによって、またはより多くの制御が必要な場合は、ある時点でそれらを強制することを決定できます。これは、システムが誰もそれを観察せずに進化するかのように、ケースで望ましい場合があります。評価されていないサンクを簡単に蓄積できます)。

複数の s を持つことが実際に重要であることが判明した場合TVar、おそらくすべてのコードをカスタムモナドに記述し (回答を書いているときに @Cirdec で説明されているように)、その実装はメインコードから隠され、これは、状態の一部を読み取る (そしておそらく書き込む) 関数を提供します。その後、単一のSTMトランザクションとして実行され、必要なものだけを読み書きし、テスト用にモナドの純粋なバージョンを作成できます。

于 2015-03-07T20:56:38.530 に答える