将状态与IO操作相结合

dan*_*tin 28 monads state haskell

假设我有一个状态monad,例如:

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)
Run Code Online (Sandbox Code Playgroud)

功能如

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

updState :: (ST -> ST) -> Op ()
updState g = Op (\st -> (g st, ()))
Run Code Online (Sandbox Code Playgroud)

等等.我想将此monad中的各种操作与IO操作结合起来.所以我可以编写一个评估循环,在该循环中执行此monad中的操作,并使用结果执行IO操作,或者,我认为,我应该能够执行以下操作:

newtype Op a = Op {runOp :: ST -> IO (ST, a)}
Run Code Online (Sandbox Code Playgroud)

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

runOp (do x <- getLine; setMem 10 ... (read x :: Int) ... ) st
Run Code Online (Sandbox Code Playgroud)

因为getLine具有类型IO Char,但是此表达式将具有类型Op Char.概括地说,我该怎么做?

Mar*_*ijn 31

使用liftIO

你已经很亲密了!你的建议

newtype Op a = Op {runOp :: ST -> IO (ST, a)}
Run Code Online (Sandbox Code Playgroud)

非常好,还有很长的路要走.

为了能够getLineOp上下文中执行,您需要将IO操作"提升" 到Opmonad中.您可以通过编写函数来完成此操作liftIO:

liftIO :: IO a -> Op a
liftIO io = Op $ \st -> do
  x <- io
  return (st, x)
Run Code Online (Sandbox Code Playgroud)

你现在可以写:

runOp (do x <- liftIO getLine; ...
Run Code Online (Sandbox Code Playgroud)

使用MonadIO类

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

import Control.Monad.Trans

class Monad m => MonadIO m where
  liftIO :: IO a -> m a
Run Code Online (Sandbox Code Playgroud)

这样你的版本liftIO就变成了一个实例MonadIO:

instance MonadIO Op where
  liftIO = ...
Run Code Online (Sandbox Code Playgroud)

使用StateT

您目前已经编写了自己的状态monad版本,专门用于声明ST.你为什么不使用标准的状态monad?它使您不必编写自己的Monad实例,这对于状态monad总是相同的.

type Op = StateT ST IO
Run Code Online (Sandbox Code Playgroud)

StateT已经有一个Monad实例和一个MonadIO实例,所以你可以立即使用它们.

Monad变形金刚

StateT是一个所谓的monad变压器.你只需要monad中的IO动作Op,所以我已经专门IO为你做了monad(参见定义type Op).但monad变换器允许你堆叠任意monad.这就是关于inverflow的内容.你可以在这里这里阅读更多相关信息.

  • 你的建议都非常好,而且我确实认识到我的'op'monad是状态monad的专用版本.但是在这个阶段,我更喜欢自己编写代码,因为它巩固了我脑海中的结构和概念.但是,感谢您将MonadIO,StateT等引入我的注意力. (2认同)

int*_*low 25

基本方法是将Opmonad 重写为monad变换器.这将允许您在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
Run Code Online (Sandbox Code Playgroud)

要注意的关键事项:

  • 使用该MonadTrans课程
  • lift函数的使用,getLine用于将getline函数从IOmonad带入Op IOmonad.

顺便提一下,如果你不想IO总是出现monad,你可以用Identitymonad 替换它Control.Monad.Identity.该Op Identity单子的行为完全一样,你原来的Op单子.

  • 我意识到(现在)提出的解决方案是标准的,但这是一个非常有趣(优雅)的解决方案.使用monad变换器重写代码非常有用,因为它提供了一个具体的例子.谢谢! (3认同)