2012-09-08 13 views
8

quando sono andato attraverso l'ultimo capitolo della Lyah e incontrato ListZipper, mi sono dato un incarico che per renderlo una monade Stato in modo che il codice sorgente apparirebbe più chiaro come:Come posso sfruttare sia State sia Writer in haskell?

manipList = do 
    goForward 
    goForward 
    goBack 

e allo stesso tempo, volevo tenere un registro per questo processo sfruttando la monade di Writer, ma non sapevo come combinare queste due Monade insieme.

La mia soluzione era quella di mantenere un [String] all'interno dello stato, e il mio codice sorgente è

import Control.Monad 
import Control.Monad.State 

type ListZipper a = ([a], [a]) 

-- move focus forward, put previous root into breadcrumbs 
goForward :: ListZipper a -> ListZipper a 
goForward (x:xs, bs) = (xs, x:bs) 

-- move focus back, restore previous root from breadcrumbs 
goBack :: ListZipper a -> ListZipper a 
goBack (xs, b:bs) = (b:xs, bs) 

-- wrap goForward so it becomes a State 
goForwardM :: State (ListZipper a) [a] 
goForwardM = state stateTrans where 
    stateTrans z = (fst newZ, newZ) where 
     newZ = goForward z 

-- wrap goBack so it becomes a State 
goBackM :: State (ListZipper a) [a] 
goBackM = state stateTrans where 
    stateTrans z = (fst newZ, newZ) where 
     newZ = goBack z 

-- here I have tried to combine State with something like a Writer 
-- so that I kept an extra [String] and add logs to it manually 

-- nothing but write out current focus 
printLog :: Show a => State (ListZipper a, [String]) [a] 
printLog = state $ \(z, logs) -> (fst z, (z, ("print current focus: " ++ (show $ fst z)):logs)) 

-- wrap goForward and record this move 
goForwardLog :: Show a => State (ListZipper a, [String]) [a] 
goForwardLog = state stateTrans where 
    stateTrans (z, logs) = (fst newZ, (newZ, newLog:logs)) where 
     newZ = goForward z 
     newLog = "go forward, current focus: " ++ (show $ fst newZ) 

-- wrap goBack and record this move 
goBackLog :: Show a => State (ListZipper a, [String]) [a] 
goBackLog = state stateTrans where 
    stateTrans (z, logs) = (fst newZ, (newZ, newLog:logs)) where 
     newZ = goBack z 
     newLog = "go back, current focus: " ++ (show $ fst newZ) 

-- return 
listZipper :: [a] -> ListZipper a 
listZipper xs = (xs, []) 

-- return 
stateZipper :: [a] -> (ListZipper a, [String]) 
stateZipper xs = (listZipper xs, []) 

_performTestCase1 = do 
    goForwardM 
    goForwardM 
    goBackM 

performTestCase1 = 
    putStrLn $ show $ runState _performTestCase1 (listZipper [1..4]) 

_performTestCase2 = do 
    printLog 
    goForwardLog 
    goForwardLog 
    goBackLog 
    printLog 

performTestCase2 = do 
    let (result2, (zipper2, log2)) = runState _performTestCase2 $ stateZipper [1..4] 
    putStrLn $ "Result: " ++ (show result2) 
    putStrLn $ "Zipper: " ++ (show zipper2) 
    putStrLn "Logs are: " 
    mapM_ putStrLn (reverse log2) 

Ma il problema è che non penso che questa è una buona soluzione dal momento che ho a mantenere la mia registra manualmente. Esiste un modo alternativo di mescolare monade di stato e monade di scrittore in modo che possano lavorare insieme?

risposta

16

Stai cercando monad transformers. L'idea di base è definire un tipo come WriterT che prende un'altra monade e la combina con una Writer creando un nuovo tipo (come WriterT log (State s)).

Nota: esiste una convenzione in cui i tipi di trasformatori terminano con un capitale T. Quindi Maybe e Writer sono monade normali e MaybeT e WriterT sono i loro equivalenti di trasformatore.

L'idea di base è molto semplice: per un gruppo di monadi, puoi facilmente immaginare di combinare il loro comportamento sul legame. L'esempio più semplice è Maybe. Ricordiamo che tutto ciò che fa è Maybe propagano Nothing il bind:

Nothing >>= f = Nothing 
Just x >>= f = f x 

quindi dovrebbe essere facile immaginare che si estende qualsiasi monade con questo comportamento. Tutto ciò che facciamo è controllare prima Nothing e poi usare il vecchio bind del monade. Il tipo MaybeT fa esattamente questo: avvolge una monade esistente e prefigura ogni bind con un controllo come questo. È inoltre necessario implementare return essenzialmente avvolgendo il valore in un Just e quindi utilizzando lo return10 della monade interna. C'è anche un po 'più di impianto idraulico per far funzionare tutto, ma questa è l'idea importante.

Si può immaginare un comportamento molto simile per Writer: prima si combinano tutti i nuovi output, quindi si utilizza il bind del vecchio monade. Questo è essenzialmente il comportamento di WriterT. Ci sono altri dettagli coinvolti, ma l'idea di base è abbastanza semplice e utile.

I trasformatori di Monad sono un modo molto comune di "combinare" le monadi come volete. Ci sono versioni delle monadi più comunemente usate come trasformatori, con la notevole eccezione di IO che deve sempre essere alla base del tuo stack monad. Nel tuo caso, sia WriterT sia StateT esistono e potrebbero essere utilizzati per il tuo programma.

+0

questo è esattamente quello che voglio!Ho provato e tutti e due (WriterT e StateT) funzionano bene, ty! – Javran

12

Tikhon Jelvis dà una bella risposta con i trasformatori monad. Tuttavia, c'è anche una soluzione rapida.

Il modulo Control.Monad.RWS in mtl esporta il RWS Monade, che è una combinazione delle Reader, Writer e State monadi.

Problemi correlati