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 같은 모나드 인스턴스의 다른 구현, 또는 의미 론적 해석의 가능성을 제공해야합니다, 무료 모나드 있습니다. 나는이에 가지고 올 수있는 가장 좋은 패턴은 다음과 같은 기능에 의해 주어진다 :

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. 

이 모두 잘 작동하지만 그것은 나에게이 모 될 수 있다는 약간 불안한 느낌을 준다 일반적으로, 또는 더 잘 조직 될 수 있습니다. 계속을 제공하는 기능을 제공하는 것이 처음에는 분명하지 않았으며 이것이 최선의 접근 방식인지 확신 할 수 없습니다. 나는 foldFreeinduce과 같은 Control.Monad.Free 모듈의 기능면에서 interpret을 재정의하려는 몇 가지 노력을했다. 하지만 그들은 모두 잘 작동하지 않는 것 같습니다.

나는 이것과 함께 올바른 행에 있습니까, 아니면 오판을하고 있습니까? 내가 찾은 무료 모나드에 대한 대부분의 기사는 실제로 이와 같은 것을 사용하기위한 패턴보다는 효율성을 높이거나 구현하는 다양한 방법에 중점을 둡니다.

Semantic 클래스에서 이것을 캡슐화하는 것이 바람직하기 때문에 newtype에 functor를 래핑하고이 클래스의 인스턴스로 만들어서 무료 모나드와 다른 모나드 인스턴스를 만들 수 있습니다. 그러나 나는 이것을 어떻게하는지 잘 이해할 수 없었다.

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의 무료 패키지에) :하지만 결국, 허용 대답에 편집 후 내가 있던 기능이 포함되어 있습니다.

pipes, operationalsacundim's free-operational package의 세 가지 모두 매우 관련성이 높으며 나중에 나에게 유용 할 것으로 보입니다. 다들 감사 해요.

답변

3

나는 당신의 예제를 이해하지 못하지만 기본적으로 여기서 operational 패키지를 재구성하고 있다고 생각합니다. EntityUpdate 유형은 operational의 명령어 세트와 매우 비슷하며 UpdateFunctor은 명령어 세트에 대한 무료 함수기와 같습니다. 정확히 operational과 무료 모나드를 연결하는 구조입니다. ("Is operational really isomorphic to a free monad?"this Reddit discussion 참조).

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

이 당신이 모나드 작업으로 프로그램의 지침을 각각 해석 기능 (각 EntityUpdate 값)을 제공 할 수 있습니다 :

그러나 어쨌든 operational 패키지는 원하는 기능 interpretWithMonad있다 , 나머지는 처리합니다. 내가 자기 홍보의 조금을 허용 될 수있는 경우

, 내가 operationalProgram 유형의 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 (원래 하나 또는 내 것)을 가져오고을 사용하는 것입니다.대신 ProgramA. 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 

편집 : 돌이켜 보면 나는이 짧은 대답을 제공해야합니다. 이것은 에드워드 크멧의 패키지에서 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 interpret'의 다른 버전을 매우 열심히 봐야 할 시간입니다 ... –

7

무료 모나 크 작업을 위해 더 높은 수준의 추상화를 제공하는 내 pipes 라이브러리를 사용할 수 있습니다.

pipes

이 계산의 모든 부분을 구체화하기 위해 무료 모나드를 사용

  • 데이터의 Producer (즉, 당신의 갱신은) 무료 모나드에게
  • 데이터의 Consumer (즉, 귀하의 통역) 무료입니다 모나드
  • 데이터의 Pipe (즉, 로거)는

사실, 그들은 세 SEPA없는 무료 모나드이다 요금 무료 모나드 : 그들은 모두 모나드와 동일한 무료 모나드입니다. 세 가지를 모두 정의하고 나면 스트리밍 데이터를 시작하기 위해 파이프 구성 인 (>->)을 사용하여 연결합니다.

나는 당신이 쓴 유형 클래스 건너 뛰고 당신의 예제의 약간 수정 된 버전으로 시작할 것이다 :

: 이제

{-# 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의의 수하는 Update를 정의

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

그런 다음 실제 명령을 정의합니다. 각 명령은 respond 파이프 기본을 사용하여 해당 업데이트를 산출하며, 처리를 위해 데이터를 더 하류로 보냅니다. Producer은 무료 모나드이기 때문에

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

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

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

, 우리는 당신이 당신의 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} 

우리는 왜 이것을 두 단계로 수행해야하는지 궁금해 할 것입니다.왜 runProxy 부분을 제거하지 않는 것이 좋을까요?

이유는 우리가 두 가지 이상을 작성하고자 할 수있는 이유입니다. 예를 들어 testeval 사이에 로깅 단계를 매우 쉽게 삽입 할 수 있습니다. tell이 값의 표현이야, 그것은 request에 값이야하고 respond를 사용하여 값 하류 통과 : 우리는 매우 명확 logger가 무엇을 볼 수있는, 다시

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 

: 나는이 중간 단계를 Pipe의 전화.

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" 

설명하는 문제의 유형을 정확히 해결하기 위해 설계되었습니다. 우리가 데이터를 생성하는 DSL뿐만 아니라 해석기와 중간 처리 단계까지도 알리고 싶어하는 많은 시간. pipes은 이러한 모든 개념을 동일하게 취급하고 이들 모두를 연결 가능한 스트림 DSL로 모델링합니다. 따라서 사용자 정의 인터프리터 프레임 워크를 정의하지 않고도 다양한 동작을 쉽게 교환 할 수 있습니다.

파이프를 처음 사용하는 사람이라면 tutorial을 확인해 볼 수 있습니다.

+0

나는 전에 '파이프'를 만났습니다. 이제는 제대로 이해할 시간을 가질 계획입니다. IO없이 순수 State Monads를 사용하여 통역사를 작성할 수 있습니까? 그러나,'pipe'의 모든 기능은 내가 찾고있는 것보다 정확히 헤비급이라고 생각합니다. 이런 식으로 관심을 분리하기 위해 무료 모나드를 사용하는 것에 대한 최소한의 이론적 근거와 같습니다. 저는 여러분의 모범을 철저히 살펴보고 파이프가 만들어지는 방식을 살펴 보겠습니다. –

+0

기본 모나드는 'IO'를 사용할 필요가없는 경우 순수한'State' 모나드를 포함하여 무엇이든 될 수 있습니다. 'pipes'는 실제로 가장 가벼운 coroutine 라이브러리입니다. 작문은 [5 행의 코드] (http://hackage.haskell.org/packages/archive/pipes/3.2.0/doc/html/src/Control-Proxy-Core-Fast.html)이며 그 밖의 모든 것은 다시 쓰기 규칙을 사용하여보다 효율적인 무료 모나드를 다시 구현하는 것입니다. 더 많은 기능을 제공하는 이유는 필자가 옳은 추상화를 찾기 위해 많은 시간을 보냈기 때문입니다. –