2010-09-03 53 views
26

假设我有一个单子状态如:结合状态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) 

与功能,如

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

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

等等。我想将这个monad中的各种操作与IO操作结合起来。因此,我既可以写在这个单子的操作进行,并用结果被执行的IO操作的评价循环,或者,我想,我应该能够做到像下面这样:

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

印刷函数的类型为Op(),其他函数的类型为Op a,例如,我可以使用IO Char类型的函数从终端读取一个字符。但是,我不确定这样的功能是什么样子的,因为例如以下是无效的。

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

因为getLine的类型为IO Char,但该表达式的类型为Op Char。概述,我将如何做到这一点?

回答

25

的基本方法是重写你Op单子一个单子转换。这可以让你在一堆“monad”中使用它,它的底部可能是IO

下面是什么,可能看起来像一个例子:

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 

关键的东西来观察:

  • 使用该MonadTrans
  • 作用于使用lift功能getLine,用于将getline函数从IO monad和Op IO monad中提取出来。

顺便说一句,如果你不希望IO单子永远存在,你可以用Identity单子在Control.Monad.Identity更换。 Op Identity monad的行为与原始Op monad的行为完全相同。

+3

我意识到(现在)所提出的解决方案是标准的,但这是一个非常有趣(优雅)的解决方案。使用monad变换器重写我的代码非常有用,因为它提供了一个具体示例。谢谢! – danportin 2010-09-04 22:15:55

+0

Martijn的实用解决方案是一个很好的教育补充。 – 2015-08-24 08:33:31

26

使用liftIO

你已经非常接近!你的建议

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

是优秀的和走的路。

为了能够在Op上下文中执行getLine,你需要“电梯”的IO操作进入Op单子。

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

您现在可以这样写::

使用类MonadIO

现在提起一个IO动作到定制单子的模式,您可以通过编写一个函数liftIO做到这一点是如此普遍,以至于有一个标准类型的它:

import Control.Monad.Trans 

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

使您的liftIO版本成为MonadIO一个实例,而不是:

instance MonadIO Op where 
    liftIO = ... 

使用StateT

目前你写你自己的状态单子的版本,专门的国家ST。你为什么不使用标准状态monad?它使您不必编写自己的Monad实例,该实例对于状态monad而言总是相同的。

type Op = StateT ST IO 

StateT已经有一个Monad实例和MonadIO实例,让您可以立即使用这些。

Monad的变压器

StateT是所谓单子转换。您只需要在您的Op monad中执行IO操作,因此我已经将它专门用于您的IO monad(请参阅type Op的定义)。但monad变换器允许你堆叠任意monads。这就是流入的内容。你可以阅读更多关于他们herehere

+2

你的建议非常好,我意识到我的'op'monad是国家monad的专门版本。但在这个阶段,我更喜欢自己编写代码,因为它巩固了我脑海中的结构和概念。但是,感谢您将MonadIO,StateT等等引入我的视线。 – danportin 2010-09-04 22:18:20