将状态与IO操作结合

35

假设我有一个状态单子,如下所示:

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, ()))

等等。我想要将不同的操作与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。简单说,我该怎么做?

2个回答

37

使用liftIO

你已经非常接近了!按照你的建议

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

很棒,是前进的道路。

要在Op上下文中执行getLine,您需要将IO操作提升到Op单子中。您可以编写一个名为liftIO的函数来实现此操作:

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

现在您可以编写:

runOp (do x <- liftIO getLine; ...

使用MonadIO类

现在将IO操作提升为自定义monad的模式非常普遍,因此有一个标准类型类可用于此:

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实例,因为对于状态单子来说,它总是相同的。

type Op = StateT ST IO

StateT已经有Monad实例和MonadIO实例,因此您可以立即使用它们。

Monad变换器

StateT是一种所谓的monad变换器。您只需在Op monad中使用IO操作,因此我已为您特化了它(请参见type Op的定义)。但是,monad变换器允许您堆叠任意的monad。这是stackoverflow正在讨论的内容。您可以在这里这里阅读更多信息。


2
您提出的建议都非常棒,我确实意识到我的“op”单子是状态单子的一个专用版本。但在这个阶段,我更愿意自己编写代码,因为这有助于把结构和概念牢固地铭记于心。不过,感谢您让我注意到MonadIO、StateT等工具。 - emi

29

基本的方法是将你的Op单子改写成一个单子变换器。这样可以使你在单子"栈"中使用它,而底层可能是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
  • 使用作用于 getLinelift 函数,它将 IO 中的 getLine 函数转换到 Op IO 单子中。

顺带一提,如果你不想一直使用 IO 单子,可以用 Control.Monad.Identity 中的 Identity 单子替换它。这时,Op Identity 单子与原来的 Op 单子具有完全相同的行为。


4
我现在意识到这个解决方案是标准的,但这是一个非常有趣(而且优雅)的解决方案。你使用单子变换器重写我的代码非常有帮助,因为它提供了一个具体的例子。谢谢! - emi
Martijn的实用解决方案的一个不错的教育补充。 - Erik Kaplun

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接