2010-09-03 5 views
26

Supposons que j'ai une monade de l'Etat tels que:conjugueront avec des actions 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) 

avec des fonctions telles que

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

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

et ainsi de suite. Je veux combiner diverses opérations dans cette monade avec des actions d'E/S. Je pourrais donc soit écrire une boucle d'évaluation dans lequel les opérations dans cette monade ont été effectuées et une action IO est exécutée avec le résultat, ou, je pense, je devrais être en mesure de faire quelque chose comme ce qui suit:

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

Impression les fonctions auraient le type Op() et d'autres fonctions auraient le type Op a, par exemple, je pourrais lire un caractère du terminal en utilisant une fonction de type IO Char. Cependant, je ne suis pas sûr de savoir à quoi ressemblerait une telle fonction, puisque, par exemple, ce qui suit n'est pas valide.

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

puisque getLine a le type IO Char, mais cette expression aurait le type Op Char. Dans les grandes lignes, comment ferais-je cela?

Répondre

25

L'approche de base serait de réécrire votre monade Op comme transformateur monad. Cela vous permettrait de l'utiliser dans une "pile" de monades, dont le fond pourrait être IO.

Voici un exemple de ce qui pourrait ressembler à:

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 

Les principales choses à observer:

  • L'utilisation de la classe MonadTrans
  • L'utilisation de la fonction lift agissant sur , qui est utilisée pour amener la fonction getline de la monade IO et dans la monade Op IO.

Soit dit en passant, si vous ne voulez pas le IO monade être toujours présent, vous pouvez le remplacer par le Identity monade dans Control.Monad.Identity. La monade Op Identity se comporte exactement comme votre monade Op originale.

+3

Je réalise (maintenant) que la solution présentée est standard, mais c'est une solution très intéressante (et élégante). Votre réécriture de mon code à l'aide d'un transformateur monad a été très utile, car elle a fourni un exemple concret. Merci! – danportin

+0

Un bon complément éducatif à la solution pragmatique de Martijn. –

26

Utilisez liftIO

Vous êtes déjà très proche! Votre suggestion

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

est excellent et le chemin à parcourir.

Pour pouvoir exécuter getLine dans un contexte Op, vous devez « ascenseur » l'opération IO dans le Op monade.Vous pouvez le faire en écrivant une fonction liftIO:

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

Vous pouvez maintenant écrire:

Classe d'MonadIO

Maintenant, le modèle de soulever une action IO dans une monade personnalisée est si commun qu'il existe une classe de type standard pour cela:

import Control.Monad.Trans 

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

Pour que votre version de liftIO devient une instance de MonadIO à la place:

instance MonadIO Op where 
    liftIO = ... 

Utilisez StateT

Vous avez actuellement écrit votre propre version de la monade de l'Etat, spécialisé à l'état ST. Pourquoi n'utilisez-vous pas la monade d'état standard? Cela vous évite d'avoir à écrire votre propre instance Monad, ce qui est toujours le même pour la monade d'état.

type Op = StateT ST IO 

StateT a déjà une instance Monad et une instance MonadIO, de sorte que vous pouvez les utiliser immédiatement.

transformateurs Monad

StateT est un soi-disant transformateur monade. Vous ne voulez que IO actions dans votre monade Op, donc je l'ai déjà spécialisé avec la monade IO pour vous (voir la définition de type Op). Mais les transformateurs monad vous permettent d'empiler des monades arbitraires. C'est ce dont parle l'intoverflow. Vous pouvez en lire plus à leur sujet here et here.

+2

Vos suggestions sont toutes excellentes, et je me rends compte que mon monade «op» est une version spécialisée de la monade d'état. Mais à ce stade, je préfère écrire le code moi-même, car il cimente la structure et les concepts dans ma tête. Cependant, je vous remercie d'avoir porté MonadIO, StateT, et ainsi de suite, à mon attention. – danportin