9

私は、特定のセマンティクスをファンクター上のフリー モナドに適用するパターンを抽象化しようとしています。これを動機付けるために私が使用している実行中の例は、ゲーム内のエンティティに更新を適用することです。そこで、いくつかのライブラリをインポートし、この例の目的のためにいくつかの型とエンティティ クラスを定義します (私は control-monad-free で free モナド実装を使用しています):

{-# LANGUAGE DeriveFunctor #-}
{-# LANGUAGE TypeFamilies #-}

import Control.Monad.Free
import Control.Monad.Identity
import Control.Monad.Writer

-- Things which can happen to an entity
data Order = Order deriving Show
data Damage = Damage deriving Show

class Entity a where
    evolve :: Double -> a -> a
    order :: Order -> a -> a
    damage :: Damage -> a -> a

-- Make a trivial entity for testing purposes
data Example = Example deriving Show
instance Entity Example where
    evolve _ a = a
    order _ a = a
    damage _ a = a

-- A type to hold all the possible update types
data EntityUpdate = 
      UpdateTime Double
    | UpdateOrder Order
    | UpdateDamage Damage
    deriving (Show)

-- Wrap UpdateMessage to create a Functor for constructing the free monad
data UpdateFunctor cont = 
    UpdateFunctor {updateMessage :: EntityUpdate, continue :: cont} deriving (Show, Functor)

-- Type synonym for the free monad
type Update = Free UpdateEntity

ここで、いくつかの基本的な更新をモナドに持ち上げます。

liftF = wrap . fmap Pure

updateTime :: Double -> Update ()
updateTime t = liftUpdate $ UpdateTime t

updateOrder :: Order -> Update ()
updateOrder o = liftUpdate $ UpdateOrder o

updateDamage :: Damage -> Update ()
updateDamage d = liftUpdate $ UpdateDamage d

test :: Update ()
test = do
    updateTime 8.0
    updateOrder Order
    updateDamage Damage
    updateTime 4.0
    updateDamage Damage
    updateTime 6.0
    updateOrder Order
    updateTime 8.0

これで自由なモナドができたので、上記のようなモナドインスタンスのさまざまな実装または意味解釈の可能性を提供する必要がありますtest。これに対して私が思い付くことができる最良のパターンは、次の関数によって与えられます。

interpret :: (Monad m, Functor f, fm ~ Free f c) => (f fm -> fm) -> (f fm -> a -> m a) -> fm -> a -> m a
interpret _ _ (Pure _  ) entity = return entity
interpret c f (Impure u) entity = f u entity >>= interpret c f (c u)

次に、いくつかの基本的なセマンティック関数を使用して、次の 2 つの可能な解釈を与えることができます。1 つは基本的な評価として、もう 1 つはロギングを実行するライター モナドとしてです。

update (UpdateTime t) = evolve t
update (UpdateOrder o) = order o
update (UpdateDamage d) = damage d

eval :: Entity a => Update () -> a -> a
eval updates entity = runIdentity $ interpret continue update' updates entity where
    update' u entity = return $ update (updateMessage u) entity

logMessage (UpdateTime t) = "Simulating time for " ++ show t ++ " seconds.\n"
logMessage (UpdateOrder o) = "Giving an order.\n"
logMessage (UpdateDamage d) = "Applying damage.\n"

evalLog :: Entity a => Update () -> a -> Writer String a
evalLog = interpret continue $ \u entity -> do
    let m = updateMessage u
    tell $ logMessage m
    return $ update m entity

これを GHCI でテストします。

> eval test Example
Example
> putStr . execWriter $ evalLog test Example
Simulating time for 8.0 seconds.
Giving an order.
Applying damage.
Simulating time for 4.0 seconds.
Applying damage.
Simulating time for 6.0 seconds.
Giving an order.
Simulating time for 8.0 seconds.

これはすべて正常に機能しますが、より一般的であるか、よりよく整理されている可能性があるのではないかと少し不安に感じます. 継続を提供する関数を提供する必要があることは、最初は明らかではありませんでした。それが最善のアプローチであるかどうかはわかりません。やinterpretなど、Control.Monad.Free モジュールの関数に関して再定義するためにいくつかの努力をしました。しかし、それらはすべてうまく機能していないようです。foldFreeinduce

私はこれで正しい方向に進んでいますか、それとも判断を誤っていますか? 私が見つけた自由なモナドに関する記事のほとんどは、このように実際にそれらを使用するためのパターンではなく、それらの効率またはそれらを実装するさまざまな方法に集中しています。

これをある種のSemanticクラスにカプセル化することも望ましいと思われるので、ファンクタを newtype でラップしてこのクラスのインスタンスにすることで、フリー モナドから別のモナド インスタンスを簡単に作成できます。しかし、私はこれを行う方法を完全に理解できませんでした。

アップデート -

どちらも非常に有益で思慮深く書かれているので、両方の回答を受け入れることができればよかったのにと思います。ただし、最終的には、受け入れられた回答の編集には、私が求めていた機能が含まれています。

interpret :: (Functor m, Monad m) => (forall x. f x -> m x) -> Free f a -> m a
interpret evalF = retract . hoistFree evalF

(retractそしてhoistFreeControl.Monad.Free の Edward Kemmet の無料パッケージに含まれています)。

の 3 つすべてpipessacundim の free-operational パッケージoperationalは 非常に関連性が高く、将来的には非常に役立つようです。皆さん、ありがとうございました。

4

2 に答える 2

3

あなたの例はよくわかりませんが、基本的operationalにここでパッケージを再構築していると思います。あなたのEntityUpdate型は という意味で命令セットに非常によく似ており、operationalあなたの型は命令セットUpdateFunctor上のフリー ファンクターのようなoperationalものです。(「オペレーショナルはフリーモナドと本当に同形ですか?」とこの Reddit ディスカッションを参照してください)

とにかく、operationalパッケージには必要な機能がありますinterpretWithMonad

interpretWithMonad :: forall instr m b.
                      Monad m => 
                      (forall a. instr a -> m a) 
                   -> Program instr b
                   -> m b

これにより、プログラム内の各命令 (各EntityUpdate値) をモナド アクションとして解釈し、残りを処理する関数を提供できます。

ちょっとした自己宣伝が許されるなら、私はの型のバージョンが欲しかったので、つい最近free monads を使った独自のバージョンを書いてoperationalいました。あなたの例は純粋に応用的であると私には思えたので、私は自分のライブラリーに関してあなたを書く練習をしました。(私はあなたの機能を理解できませんでした。)ここに行きます:ApplicativeoperationalProgramevalLogeval

{-# LANGUAGE GADTs, ScopedTypeVariables, RankNTypes #-}

import Control.Applicative
import Control.Applicative.Operational
import Control.Monad.Writer

data Order = Order deriving Show
data Damage = Damage deriving Show

-- UpdateI is short for "UpdateInstruction"
data UpdateI a where
    UpdateTime   :: Double -> UpdateI ()
    UpdateOrder  :: Order -> UpdateI ()
    UpdateDamage :: Damage -> UpdateI ()

type Update = ProgramA UpdateI

updateTime :: Double -> Update ()
updateTime = singleton . UpdateTime

updateOrder :: Order -> Update ()
updateOrder = singleton . UpdateOrder

updateDamage :: Damage -> Update ()
updateDamage = singleton . UpdateDamage

test :: Update ()
test = updateTime 8.0 
    *> updateOrder Order
    *> updateDamage Damage
    *> updateTime 4.0
    *> updateDamage Damage
    *> updateTime 6.0
    *> updateOrder Order
    *> updateTime 8.0

evalLog :: forall a. Update a -> Writer String a
evalLog = interpretA evalI
    where evalI :: forall x. UpdateI x -> Writer String x
          evalI (UpdateTime t) = 
              tell $ "Simulating time for " ++ show t ++ " seconds.\n"
          evalI (UpdateOrder Order) = tell $ "Giving an order.\n"
          evalI (UpdateDamage Damage) = tell $ "Applying damage.\n"

出力:

*Main> putStr $ execWriter (evalLog test)
Simulating time for 8.0 seconds.
Giving an order.
Applying damage.
Simulating time for 4.0 seconds.
Applying damage.
Simulating time for 6.0 seconds.
Giving an order.
Simulating time for 8.0 seconds.

ここでのトリックは、元のパッケージの関数と同じですが、interpretWithMonadアプリケーションに適応しています:

interpretA :: forall instr f a. Applicative f =>
              (forall x. instr x -> f x)
           -> ProgramA instr a -> f a

Control.Monad.Operationalモナド解釈が本当に必要な場合は、の代わりに (元のものまたは私のもの)をインポートし、 の代わりにControl.Applicative.Operational使用するだけProgramですProgramAProgramAただし、プログラムを静的に調べるためのより強力な機能が提供されます。

-- Sum the total time requested by updateTime instructions in an
-- applicative UpdateI program.  You can't do this with monads.
sumTime :: ProgramA UpdateI () -> Double
sumTime = sumTime' . viewA 
    where sumTime' :: forall x. ProgramViewA UpdateI x -> Double
          sumTime' (UpdateTime t :<**> k) = t + sumTime' k
          sumTime' (_ :<**> k) = sumTime' k
          sumTime' (Pure _) = 0

の使用例sumTime:

*Main> sumTime test
26.0

編集:振り返ってみると、この短い回答を提供する必要がありました。これはControl.Monad.Free、Edward Kmett のパッケージから使用していることを前提としています。

interpret :: (Functor m, Monad m) =>
             (forall x. f x -> m x) 
          -> Free f a -> m a
interpret evalF = retract . hoistFree evalF
于 2013-04-11T08:37:12.020 に答える