¿Cómo podría aprovechar tanto State como Writer en haskell?

cuando pasé por el último capítulo de LYAH y me encontré con ListZipper, me asigné la tarea de convertirla en una mónada estatal para que el código fuente se viera más claro como:

manipList = do
    goForward
    goForward
    goBack

y al mismo tiempo, quería llevar un registro de este proceso aprovechando la mónada de Writer, pero no sabía cómo combinar estas dos mónadas.

Mi solución fue mantener una [String] dentro del estado, y mi código fuente es

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)

Pero el problema es que no creo que esta sea una buena solución ya que tengo que mantener mis registros manualmente. ¿Hay alguna forma alternativa de mezclar la mónada State y la mónada Writer para que puedan trabajar juntas?

preguntado el 08 de septiembre de 12 a las 09:09

2 Respuestas

Estas buscando transformadores de mónada. La idea básica es definir un tipo como WriterT que toma otra mónada y la combina con una Writer creando un nuevo tipo (como WriterT log (State s)).

Nota: existe una convención de que los tipos de transformadores terminan con mayúscula T. Así que, Maybe y Writer son mónadas normales y MaybeT y WriterT son sus equivalentes de transformador.

La idea central es muy simple: para un grupo de mónadas, puedes imaginar fácilmente combinar su comportamiento en enlace. El ejemplo más simple es Maybe. Recuerda que todo eso Maybe lo que hace es propagar Nothing en enlace:

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

Así que debería ser fácil imaginar extender cualquier mónada con este comportamiento. Todo lo que hacemos es buscar Nothing primero y luego usa el enlace de la mónada anterior. los MaybeT type hace exactamente esto: envuelve una mónada existente y antecede cada enlace con una marca como esta. También tendrías que implementar return esencialmente envolviendo el valor en un Just y luego usando la mónada interna return. También hay un poco más de plomería para que todo funcione, pero esta es la idea importante.

Puedes imaginar un comportamiento muy similar para Writer: primero combinamos cualquier salida nueva y luego usamos el enlace de la mónada anterior. Este es esencialmente el comportamiento de WriterT. Hay algunos otros detalles involucrados, pero la idea básica es bastante simple y útil.

Los transformadores de mónadas son una forma muy común de "combinar" mónadas como quieras. Hay versiones de las mónadas más utilizadas como transformadores, con la notable excepción de IO que siempre tiene que estar en la base de tu pila de mónadas. En tu caso, ambos WriterT y StateT existen y podrían usarse para su programa.

Respondido el 08 de Septiembre de 12 a las 09:09

eso es exactamente lo que quiero! Lo he intentado y ambos (WriterT & StateT) funcionan bien, ¡ty! - Javrán

Tikhon Jelvis da una buena respuesta con transformadores de mónadas. Sin embargo, también hay una solución rápida.

La Control.Monad.RWS módulo en mtl exporta el RWS mónada, que es una combinación de la Reader, Writer y State mónadas.

Respondido el 08 de Septiembre de 12 a las 10:09

No es la respuesta que estás buscando? Examinar otras preguntas etiquetadas or haz tu propia pregunta.