9

フリーマンモナドにあるファンクタを使ってあるセマンティクスを適用するパターンを抽象化しようとしています。これを動機づけするために使用している実行例は、ゲーム内のエンティティに更新を適用することです。だから私は(私がコントロール-モナドフリーで自由なモナドの実装を使用しています)いくつかのライブラリをインポートし、この例の目的のためにいくつかの例タイプとエンティティクラスを定義します。フリーモナドへのセマンティクスの適用

{-# 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モナドのインスタンスの異なる実装、または意味の解釈の可能性を提供する必要があり、無料のモナドを持っています。その後、いくつかの基本的なセマンティックな機能で、我々は次の二つの可能な解釈、基本的な評価として1とライターモナドとして1を与えることができます

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) 

:私はこのために思い付くことができる最高のパターンは以下の関数で与えられます予備成形ログ:GHCiの中

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 

テストこの:

> 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. 

このすべてが正常に動作しますが、それは私に、それはMOことができることを少し違和感を与えます一般的な、またはより良い組織することができます。継続を提供する機能を提供することは、当初は明らかではなかったし、私はそれが最良のアプローチであるかどうかはわからない。私は、foldFreeinduceのような、Control.Monad.Freeモジュールの機能に関して、interpretを再定義するいくつかの努力をしました。しかし、彼らはすべてうまくいかないようです。

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

これをある種のSemanticクラスでカプセル化することも望ましいと思われるので、新しいタイプのファンクタをラップしてこのクラスのインスタンスにすることで、フリーモナドとは異なるモナドインスタンスを作ることができます。しかし、私はこれをどうやってやっているのかはかなり分かりませんでした。

UPDATE -

私は、彼らは両方とも非常に有益で思慮深く書かれているように私は両方の答えを受け入れたことがしたいです。

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

retracthoistFreeはControl.Monad.FreeでエドワードKemmetの自由なパッケージにあります):しかし最後には、受け入れ答えを編集した後、私がいた機能が含まれています。

pipesoperationalsacundim's free-operational packageの3つはすべて、将来的に私にとって非常に有用であるように見えます。皆さん、ありがとうございました。

答えて

3

私はあなたの例をよく理解していませんが、基本的にここにoperationalパッケージを再構築していると思います。あなたのEntityUpdateタイプは、operationalという意味の命令セットに非常によく似ていて、UpdateFunctorは命令セットのフリーファンクタのようなものです。正確にはoperationalとフリーモナドに関するものです。 ("Is operational really isomorphic to a free monad?"およびthis Reddit discussionを参照)。

しかし、いずれにせよ、operationalパッケージには、あなたが望む機能を持っていinterpretWithMonad

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

これは、あなたがモナドアクションとして、プログラム内の命令のそれぞれを解釈する機能(各EntityUpdate値)を提供することができます残りの部分を処理します。私は自己宣伝の少しを許可することができる場合

、私はoperationalさんProgramタイプのApplicativeバージョンを持っていると思ったので、私は最近、my own version of operational using free monadsを書いていました。あなたの例が私を純粋に応募者にしてくれて以来、私はあなたのevalLogを私の図書館に関して書いています。私はそれをここに貼り付けることもできます。 (あなたのeval機能を理解できませんでした。)ここに行く:

{-# 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機能と同じですが、applicativesに適応:

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

あなたの場合本当にモナディックな解釈が必要なのは、Control.Applicative.Operationalの代わりにControl.Monad.Operational(元のものか鉱山のどちらか)をインポートし、の代わりに。 sumTime

-- 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 

使用例:ProgramAは、しかし、あなたに静的にプログラムを検討する大きな力を与える

*Main> sumTime test 
26.0 

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

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

これは本当に魅力的です!私がこれらの異なるバージョンの 'interpret'を非常に見ている時間。 –

7

私のpipesライブラリを使用すると、無料のモナドを扱うためのより高いレベルの抽象を提供できます。

pipes計算のすべての部分を具体化するために自由にモナドを使用しています。

  • データのProducerを(つまり、あなたの更新)データのConsumer自由モナド
  • である(つまり、あなたの通訳)は無料ですモナド
  • データのPipe(つまり、あなたのロガーが)実際には無料のモナド

して、彼らは3つのSEPAではありませんフリー・フリー・モナド:彼らはすべて、同じ偽装のフリー・モナドです。 3つすべてを定義したら、パイプ構成を使用して接続します((>->))。ストリーミングデータを開始します。私はあなたが書いた型クラススキップあなたの例を少し変更したバージョンから始めましょう

{-# LANGUAGE RankNTypes #-} 

import Control.Lens 
import Control.Proxy 
import Control.Proxy.Trans.State 
import Control.Monad.Trans.Writer 

data Order = Order deriving (Show) 
data Damage = Damage deriving (Show) 

data EntityUpdate 
    = UpdateTime Double 
    | UpdateOrder Order 
    | UpdateDamage Damage 
    deriving (Show) 

は、今、私たちは何をすべきかProducerEntityUpdateのsとUpdateを定義しています

type Update r = forall m p . (Monad m, Proxy p) => Producer p EntityUpdate m r 

次に、実際のコマンドを定義します。各コマンドは、respondパイププリミティブを使用して対応する更新を生成します。パイププリミティブは、処理のためにさらに下流にデータを送信します。

updateTime :: Double -> Update() 
updateTime t = respond (UpdateTime t) 

updateOrder :: Order -> Update() 
updateOrder o = respond (UpdateOrder o) 

updateDamage :: Damage -> Update() 
updateDamage d = respond (UpdateDamage d) 

Producerが自由モナドであるので、我々はあなたがあなたのtest機能のために行ったようdo表記を使用して、それを組み立てることができます。

test ::() -> Update() 
-- i.e.() -> Producer p EntityUpdate m() 
test() = runIdentityP $ do 
    updateTime 8.0 
    updateOrder Order 
    updateDamage Damage 
    updateTime 4.0 
    updateDamage Damage 
    updateTime 6.0 
    updateOrder Order 
    updateTime 8.0 

しかし、我々はデータのConsumerとして通訳を具体化することができます、あまりにも。これは、あなたが定義したEntityクラスを使うのではなく、インタープリタ上の状態を直接レイヤーできるからです。

私は、単純な状態使用します:

data MyState = MyState { _numOrders :: Int, _time :: Double, _health :: Int } 
    deriving (Show) 

begin :: MyState 
begin= MyState 0 0 100 

を...と明確にするため、いくつかの便利なレンズを定義します。

numOrders :: Lens' MyState Int 
numOrders = lens _numOrders (\s x -> s { _numOrders = x}) 

time :: Lens' MyState Double 
time = lens _time (\s x -> s { _time = x }) 

health :: Lens' MyState Int 
health = lens _health (\s x -> s { _health = x }) 

...そして今私はステートフルインタプリタを定義することができます。

eval :: (Proxy p) =>() -> Consumer (StateP MyState p) EntityUpdate IO r 
eval() = forever $ do 
    entityUpdate <- request() 
    case entityUpdate of 
     UpdateTime tDiff -> modify (time  +~ tDiff) 
     UpdateOrder _  -> modify (numOrders +~ 1 ) 
     UpdateDamage _  -> modify (health -~ 1 ) 
    s <- get 
    lift $ putStrLn $ "Current state is: " ++ show s 

これは、通訳者が何をしているかをはっきりさせます。受信した値をステートフルな方法で処理する方法を一目で見ることができます。

接続するには、当社のProducerConsumer我々は、ベースモナドに戻って私たちのパイプラインを変換runProxy続い(>->)組成演算子、使用:

main1 = runProxy $ evalStateK begin $ test >-> eval 

...以下の結果生成します。

>>> main1 
Current state is: MyState {_numOrders = 0, _time = 8.0, _health = 100} 
Current state is: MyState {_numOrders = 1, _time = 8.0, _health = 100} 
Current state is: MyState {_numOrders = 1, _time = 8.0, _health = 99} 
Current state is: MyState {_numOrders = 1, _time = 12.0, _health = 99} 
Current state is: MyState {_numOrders = 1, _time = 12.0, _health = 98} 
Current state is: MyState {_numOrders = 1, _time = 18.0, _health = 98} 
Current state is: MyState {_numOrders = 2, _time = 18.0, _health = 98} 
Current state is: MyState {_numOrders = 2, _time = 26.0, _health = 98} 

2つの手順でこれを行う必要があるのか​​もしれません。なぜちょうどrunProxyの部分を取り除かないのですか?

なぜ私たちは2つ以上のものを構成したいのでしょうか?たとえば、testevalの間にロギングステージを非常に簡単に挿入できます。私はこれらの中間段階Pipeの呼び出し:

再び
logger 
    :: (Monad m, Proxy p) 
    =>() -> Pipe p EntityUpdate EntityUpdate (WriterT String m) r 
logger() = runIdentityP $ forever $ do 
    entityUpdate <- request() 
    lift $ tell $ case entityUpdate of 
     UpdateTime t -> "Simulating time for " ++ show t ++ " seconds.\n" 
     UpdateOrder o -> "Giving an order.\n" 
     UpdateDamage d -> "Applying damage.\n" 
    respond entityUpdate 

を、我々は非常に明確にloggerが何をするか見ることができます:それは、request sの値は、tellは値の表現だし、その後respondを使用してさらに下流値を渡します。

testloggerの間に挿入できます。私たちは知っていなければならない唯一のものは、すべてのステージが同じベースモナドを持たなければならないということですので、我々はそれがloggerのベースモナド一致するようにevalためWriterT層を挿入するためにraiseKを使用します。

main2 = execWriterT $ runProxy $ evalStateK begin $ 
    test >-> logger >-> raiseK eval 

を...これは、次のような結果を生成します。

>>> main2 
Current state is: MyState {_numOrders = 0, _time = 8.0, _health = 100} 
Current state is: MyState {_numOrders = 1, _time = 8.0, _health = 100} 
Current state is: MyState {_numOrders = 1, _time = 8.0, _health = 99} 
Current state is: MyState {_numOrders = 1, _time = 12.0, _health = 99} 
Current state is: MyState {_numOrders = 1, _time = 12.0, _health = 98} 
Current state is: MyState {_numOrders = 1, _time = 18.0, _health = 98} 
Current state is: MyState {_numOrders = 2, _time = 18.0, _health = 98} 
Current state is: MyState {_numOrders = 2, _time = 26.0, _health = 98} 
"Simulating time for 8.0 seconds.\nGiving an order.\nApplying damage.\nSimulating time for 4.0 seconds.\nApplying damage.\nSimulating time for 6.0 seconds.\nGiving an order.\nSimulating time for 8.0 seconds.\n" 

pipesが正確にあなたが記述問題の種類を解決するために設計されました。データを生成するDSLだけでなく、インタプリタや中間処理段階も明示したいときは、多くの時間を要します。 pipesは、これらの概念のすべてを同一に扱い、すべてを接続可能なストリームDSLとしてモデル化します。これにより、独自のカスタムインタープリタフレームワークを定義しなくても、さまざまな動作の入れ替えを簡単に行うことができます。

パイプを初めてお使いの方はtutorialをチェックしてください。

+0

私はpipes'前に '会っていなかった - それは確かに非常にクールに見えます。私は今、それを正しく理解する時間をとることを計画しています。 IOなしで純粋な状態のモナドを使って通訳を書くことができますか?しかし、私は 'パイプ'の完全な機能は、ちょうど私が探しているものにちょうど重すぎると思います。これは、このように懸念を分離するためにフリーなモナドを使用するための最小限の理論的根拠に似ています。私はあなたの事例を徹底的に見ていきます。そして、「パイプ」が建設された方法でも見ていきます。 –

+0

基本モナドには、 'IO'を使う必要がなければ純粋な' State'モナドを含む何でもかまいません。 'パイプ'は実際には最も軽いコルーチンライブラリです。コンポジションは[5行のコード](http://hackage.haskell.org/packages/archive/pipes/3.2.0/doc/html/src/Control-Proxy-Core-Fast.html)のみで、その他はすべて書き換えルールを使ってより効率的なフリーモナドを再実装するだけです。それがより多くの機能を提供する理由は、私はちょうど正しい抽象化を検索するために多くの時間を費やした。 –

関連する問題