9

Sto cercando di astrarre il modello di applicare una certa semantica a una monade libera su qualche functor. L'esempio corrente che sto usando per motivare questo è applicare gli aggiornamenti a un'entità in un gioco. Così ho importare alcune librerie e definire alcuni tipi di esempio e una classe di entità ai fini di questo esempio (sto usando l'attuazione monade libera nel controllo-monade-libero):Applicare la semantica alle monadi libere

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

ora sollevare un po 'di base aggiornamenti in monade:

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 

Ora abbiamo la monade libera, abbiamo bisogno di fornire la possibilità di diverse implementazioni, o interpretazioni semantiche, di esempio monade, come test sopra. Il modello migliore che posso venire con per questo è dato dalla funzione seguente:

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) 

Poi, con alcune funzioni semantiche di base che può dare i due seguenti interpretazioni possibili, uno come valutazione di base e uno come una monade scrittore registrazione preformatura:

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 

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

questo tutto funziona benissimo, ma mi dà una sensazione un po 'a disagio che potrebbe essere mo in generale, o potrebbe essere meglio organizzato. Dover fornire una funzione per fornire la continuazione non era ovvio in un primo momento e non sono sicuro che sia l'approccio migliore. Ho fatto diversi sforzi per ridefinire interpret in termini di funzioni nel modulo Control.Monad.Free, come ad esempio foldFree e induce. Ma sembra che non tutto funzioni.

Sono sulla linea giusta con questo, o sto facendo un errore di valutazione? La maggior parte degli articoli sulle monade libere che ho trovato si concentrano sulla loro efficienza o su diversi modi per implementarli, piuttosto che su modelli per utilizzarli in questo modo.

Sembra inoltre auspicabile incapsulare questo in una sorta di classe Semantic, quindi potrei semplicemente creare diverse istanze monad dalla mia monade libera avvolgendo il functor in un newtype e rendendolo un'istanza di questa classe. Non riuscivo a capire come farlo comunque.

AGGIORNAMENTO -

vorrei potuto accettare entrambe le risposte in quanto sono entrambi estremamente informativo e pensieroso scritta. Alla fine, però, la modifica alla risposta accettata contiene la funzione che cercavo:

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

(retract e hoistFree sono in pacchetto gratuito di Edward Kemmet in Control.Monad.Free).

Tutti e tre di pipes, operational e sacundim's free-operational package sono molto pertinenti e sembrano essere molto utili per me in futuro. Grazie a tutti.

risposta

3

Non capisco il tuo esempio, ma penso che stai fondamentalmente ricostruendo il pacchetto operational qui. Il tuo tipo EntityUpdate è molto simile a un set di istruzioni nel senso di operational e il tuo UpdateFunctor è qualcosa come il functor libero sul set di istruzioni, che è precisamente la costruzione che riguarda operational e monade gratuite. (Vedere "Is operational really isomorphic to a free monad?" e this Reddit discussion).

Ma in ogni caso, il pacchetto operational ha la funzione che si desidera, interpretWithMonad:

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

Ciò consente di fornire una funzione che interpreta ciascuna delle istruzioni nel programma (ogni EntityUpdate value) come azione monadica e si prende cura del resto.

Se mi è concesso un po 'di auto-promozione, stavo solo di recente scrivendo my own version of operational using free monads, perché volevo avere una versione Applicative di s' operationalProgram tipo. Dal momento che il tuo esempio mi ha colpito come puramente applicativo, ho svolto l'esercizio di scrivere il tuo evalLog in termini di libreria, e potrei anche incollarlo qui. (Non ho capito la tua funzione eval.) Qui va:

{-# 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" 

uscita:

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

Il trucco è lo stesso come nella funzione interpretWithMonad dalla confezione originale, ma adattato per applicativi:

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

Se veramente bisogno di un'interpretazione monadica è solo una questione di importazione Control.Monad.Operational (sia quella originale o la mia) invece di Control.Applicative.Operational, e utilizzando Program anziché ProgramA. ProgramA però ti dà una maggiore potenza per esaminare il programma staticamente:

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

utilizzo Esempio di sumTime:

*Main> sumTime test 
26.0 

EDIT: Col senno di poi, avrei fornito questa risposta più brevi. Questo presuppone che tu stia usando Control.Monad.Free dal pacchetto di Edward Kmett:

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

Questo è davvero affascinante! Tempo per me di dare un'occhiata a queste diverse versioni di 'interpret' ... –

7

È possibile utilizzare la mia libreria pipes, che fornisce astrazioni di livello superiore per lavorare con le monade gratuite.

pipes utilizza monadi gratuiti a reificare ogni parte del calcolo:

  • La Producer dei dati (ad esempio l'aggiornamento) è una monade libera
  • La Consumer dei dati (cioè il vostro interprete) è un libero monade
  • La Pipe dei dati (cioè il vostro logger) è una monade libera

In realtà, non vi sono tre sepa valuta le monadi libere: sono tutte la stessa monade libera sotto mentite spoglie. Una volta definiti tutti e tre, li colleghi usando la composizione del tubo, (>->), per avviare lo streaming dei dati.

Inizierò con una versione leggermente modificata del vostro esempio che salta la classe del tipo che hai scritto:

{-# 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) 

Ora quello che facciamo è definire un Update essere un Producer di EntityUpdate s:

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

Quindi definiamo i comandi effettivi. Ogni comando produce l'aggiornamento corrispondente utilizzando la primitiva del tubo respond, che invia i dati più a valle per l'elaborazione.

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

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

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

Dal momento che un Producer è una monade libera, siamo in grado di assemblare utilizzando do la notazione proprio come avete fatto per il vostro test funzione:

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 

Tuttavia, siamo in grado di reificare l'interprete come Consumer dei dati , pure. Questo è bello perché possiamo quindi applicare direttamente sullo stato sopra l'interprete invece di usare la classe Entity che hai definito.

userò una semplice condizione:

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

begin :: MyState 
begin= MyState 0 0 100 

... e definire alcuni obiettivi convenienti per chiarezza:

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

... e ora posso definire un interprete stateful:

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 

Ciò rende molto più chiaro ciò che l'interprete sta facendo. Possiamo vedere a colpo d'occhio come elabora i valori in entrata in modo statico.

Per collegare il nostro Producer e Consumer usiamo l'operatore (>->) composizione, seguita da runProxy, che trasforma la nostra pipeline di nuovo alla monade di base:

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

... che produce il seguente risultato:

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

Ci si potrebbe chiedere perché dobbiamo farlo in due passaggi.Perché non sbarazzarsi semplicemente della parte runProxy?

Il motivo è che potremmo desiderare di comporre più di due cose. Ad esempio, possiamo facilmente inserire una fase di registrazione tra test e eval. Chiamo queste fasi intermedie Pipe s:

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 

nuovo, possiamo chiaramente vedere che logger fa: Si request s un valore, tell s una rappresentazione del valore, e quindi passa il valore più a valle con respond.

Possiamo inserire questo tra test e logger. L'unica cosa che dobbiamo essere consapevoli è che tutte le fasi devono avere la stessa Monade di base, in modo da usare raiseK per inserire uno strato WriterT per eval in modo che corrisponda la monade base del logger:

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

... che produce il seguente risultato:

>>> 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 è stato progettato per risolvere esattamente il tipo di problema che descrivi. Un sacco di volte vogliamo reificare non solo la DSL che genera i dati, ma anche gli interpreti e le fasi di elaborazione intermedie. pipes tratta tutti questi concetti in modo identico e li modella tutti come DSL di flusso collegabili. In questo modo è molto facile scambiare ed estrarre vari comportamenti senza dover definire il proprio framework di interprete personalizzato.

Se non si conosce la tubatura, è possibile controllare lo tutorial.

+0

Non avevo ancora incontrato "pipe" prima - sembra davvero molto bello. Ora ho intenzione di prendermi il tempo per comprenderlo correttamente. Si può scrivere l'interprete usando le monade di stato pure senza IO? Tuttavia, penso che la piena funzionalità di 'pipe' sia leggermente pesante per essere * esattamente * quello che sto cercando, che direi più simile a una base teorica minima per l'utilizzo di monade libere per separare le preoccupazioni in questo modo. Osserverò attentamente il tuo esempio e anche il modo in cui 'pipe' è costruito. –

+0

La monade di base può essere qualsiasi cosa, inclusa una monade 'State' pura se non è necessario usare' IO'. 'pipes' è in realtà la libreria di coroutine più leggera. Composition è solo [5 line of code] (http://hackage.haskell.org/packages/archive/pipes/3.2.0/doc/html/src/Control-Proxy-Core-Fast.html) e tutto il resto è semplicemente reimplementando una monade gratuita più efficiente usando le regole di riscrittura. Il motivo per cui offre più funzionalità è che ho passato molto tempo a cercare la giusta astrazione. –