私は、特定のセマンティクスをファンクター上のフリー モナドに適用するパターンを抽象化しようとしています。これを動機付けるために私が使用している実行中の例は、ゲーム内のエンティティに更新を適用することです。そこで、いくつかのライブラリをインポートし、この例の目的のためにいくつかの型とエンティティ クラスを定義します (私は 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 モジュールの関数に関して再定義するためにいくつかの努力をしました。しかし、それらはすべてうまく機能していないようです。foldFree
induce
私はこれで正しい方向に進んでいますか、それとも判断を誤っていますか? 私が見つけた自由なモナドに関する記事のほとんどは、このように実際にそれらを使用するためのパターンではなく、それらの効率またはそれらを実装するさまざまな方法に集中しています。
これをある種のSemantic
クラスにカプセル化することも望ましいと思われるので、ファンクタを newtype でラップしてこのクラスのインスタンスにすることで、フリー モナドから別のモナド インスタンスを簡単に作成できます。しかし、私はこれを行う方法を完全に理解できませんでした。
アップデート -
どちらも非常に有益で思慮深く書かれているので、両方の回答を受け入れることができればよかったのにと思います。ただし、最終的には、受け入れられた回答の編集には、私が求めていた機能が含まれています。
interpret :: (Functor m, Monad m) => (forall x. f x -> m x) -> Free f a -> m a
interpret evalF = retract . hoistFree evalF
(retract
そしてhoistFree
Control.Monad.Free の Edward Kemmet の無料パッケージに含まれています)。
の 3 つすべてpipes
とsacundim の free-operational パッケージoperational
は 非常に関連性が高く、将来的には非常に役立つようです。皆さん、ありがとうございました。