2010-09-03 24 views
26

Supongamos que tengo una mónada estado tales como:Combinar Estado con acciones IO

data Registers = Reg {...} 

data ST = ST {registers :: Registers, 
       memory :: Array Int Int} 

newtype Op a = Op {runOp :: ST -> (ST, a)} 

instance Monad Op where 
return a = Op $ \st -> (st, a) 
(>>=) stf f = Op $ \st -> let (st1, a1) = runOp stf st 
           (st2, a2) = runOp (f a1) st1 
          in (st2, a2) 

con funciones como

getState :: (ST -> a) -> Op a 
getState g = Op (\st -> (st, g st) 

updState :: (ST -> ST) -> Op() 
updState g = Op (\st -> (g st,())) 

y así sucesivamente. Quiero combinar varias operaciones en esta mónada con acciones IO. Así que podría o bien escribir un bucle de evaluación en el que se realizaron las operaciones en esta mónada y se ejecuta una acción IO con el resultado, o, creo, debería ser capaz de hacer algo como lo siguiente:

newtype Op a = Op {runOp :: ST -> IO (ST, a)} 

Impresión las funciones tendrían el tipo Op() y otras funciones tendrían el tipo Op a, por ejemplo, podría leer un carácter del terminal usando una función del tipo IO Char. Sin embargo, no estoy seguro de cómo se vería esa función, ya que, por ejemplo, lo siguiente no es válido.

runOp (do x <- getLine; setMem 10 ... (read x :: Int) ...) st 

ya que getLine tiene tipo IO Char, pero esta expresión tendría tipo Op Char. En resumen, ¿cómo podría hacer esto?

Respuesta

25

El enfoque básico sería reescribir su mónada Op como un transformador de mónada. Esto le permitiría usarlo en una "pila" de mónadas, la parte inferior podría ser IO.

He aquí un ejemplo de lo que podría ser:

import Data.Array 
import Control.Monad.Trans 

data Registers = Reg { foo :: Int } 

data ST = ST {registers :: Registers, 
       memory :: Array Int Int} 

newtype Op m a = Op {runOp :: ST -> m (ST, a)} 

instance Monad m => Monad (Op m) where 
return a = Op $ \st -> return (st, a) 
(>>=) stf f = Op $ \st -> do (st1, a1) <- runOp stf st 
           (st2, a2) <- runOp (f a1) st1 
           return (st2, a2) 

instance MonadTrans Op where 
    lift m = Op $ \st -> do a <- m 
          return (st, a) 

getState :: Monad m => (ST -> a) -> Op m a 
getState g = Op $ \st -> return (st, g st) 

updState :: Monad m => (ST -> ST) -> Op m() 
updState g = Op $ \st -> return (g st,()) 

testOpIO :: Op IO String 
testOpIO = do x <- lift getLine 
       return x 

test = runOp testOpIO 

Las cosas importantes a observar:

  • El uso de la clase MonadTrans
  • El uso de la función lift que actúa sobre getLine, que se utiliza para llevar la función getline de la mónada IO a la mónada Op IO.

Por cierto, si no desea que el IO mónada de estar siempre presente, puede sustituirlo por el Identity mónada en Control.Monad.Identity. La mónada Op Identity se comporta exactamente igual que su mónada original Op.

+3

Me doy cuenta (ahora) de que la solución presentada es estándar, pero esta es una solución muy interesante (y elegante). Su reescritura de mi código usando un transformador de mónada fue muy útil, ya que proporcionó un ejemplo concreto. ¡Gracias! – danportin

+0

Un buen complemento educativo a la solución pragmática de Martijn. –

26

Uso liftIO

Ya está muy cerca! Su sugerencia

newtype Op a = Op {runOp :: ST -> IO (ST, a)} 

es excelente y el camino a seguir.

Para poder ejecutar en un contexto getLineOp, es necesario 'levantar' la operación IO en el Op mónada.Puede hacer esto escribiendo una función liftIO:

liftIO :: IO a -> Op a 
liftIO io = Op $ \st -> do 
    x <- io 
    return (st, x) 

ahora se puede escribir:

Uso clase MonadIO

Ahora el patrón de levantar una acción IO en una mónada personalizada es tan común que hay una clase de tipo estándar para él:

import Control.Monad.Trans 

class Monad m => MonadIO m where 
    liftIO :: IO a -> m a 

Así que su versión de liftIO se convierte en una instancia de MonadIO lugar:

instance MonadIO Op where 
    liftIO = ... 

Uso Statet

actualmente ha escrito su propia versión de la mónada estado, especializado para el estado ST. ¿Por qué no usas la mónada de estado estándar? Le ahorra tener que escribir su propia instancia de Monad, que siempre es la misma para la mónada de estado.

type Op = StateT ST IO 

StateT ya tiene una instancia y una instancia MonadMonadIO, lo que puede utilizar los inmediatamente.

transformadores Monad

StateT es una llamada transformador mónada. Solo desea IO acciones en su mónada Op, por lo que ya lo he especializado con la mónada IO (consulte la definición de type Op). Pero los transformadores de mónada le permiten apilar mónadas arbitrarias. Esto de lo que está hablando intoverflow. Puede leer más sobre ellos here y here.

+2

Sus sugerencias son todas excelentes, y me doy cuenta de que mi 'op' mónada es una versión especializada de la mónada estatal. Pero en esta etapa, prefiero escribir el código yo mismo, ya que cementa la estructura y los conceptos en mi cabeza. Sin embargo, gracias por traer a MonadIO, StateT, etc., a mi atención. – danportin