Combine state with IO actions

2019-03-08 14:36发布

Suppose I have a state monad such as:

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)

with functions like

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

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

and so forth. I want to combine various operations in this monad with IO actions. So I could either write an evaluation loop in which operations in this monad were performed and an IO action is executed with the result, or, I think, I should be able to do something like the following:

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

Printing functions would have type Op () and other functions would have type Op a, e.g., I could read a character from the terminal using a function of type IO Char. However, I'm not sure what such a function would look like, since e.g., the following is not valid.

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

since getLine has type IO Char, but this expression would have type Op Char. In outline, how would I do this?

2条回答
Ridiculous、
2楼-- · 2019-03-08 15:30

The basic approach would be to rewrite your Op monad as a monad transformer. This would allow you to use it in a "stack" of monads, the bottom of which might be IO.

Here's an example of what that might look like:

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

The key things to observe:

  • The use of the MonadTrans class
  • The use of the lift function acting on getLine, which is used to bring the getline function from the IO monad and into the Op IO monad.

Incidentally, if you don't want the IO monad to always be present, you can replace it with the Identity monad in Control.Monad.Identity. The Op Identity monad behaves exactly the same as your original Op monad.

查看更多
放荡不羁爱自由
3楼-- · 2019-03-08 15:34

Use liftIO

You're already very close! Your suggestion

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

is excellent and the way to go.

To be able to execute getLine in an Op context, you need to 'lift' the IO operation into the Op monad. You can do this by writing a function liftIO:

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

You can now write:

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

Use class MonadIO

Now the pattern of lifting an IO action into a custom monad is so common that there is a standard type class for it:

import Control.Monad.Trans

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

So that your version of liftIO becomes an instance of MonadIO instead:

instance MonadIO Op where
  liftIO = ...

Use StateT

You've currently written your own version of the state monad, specialised to state ST. Why don't you use the standard state monad? It saves you from having to write your own Monad instance, which is always the same for the state monad.

type Op = StateT ST IO

StateT already has a Monad instance and a MonadIO instance, so you can use those immediately.

Monad transformers

StateT is a so-called monad transformer. You only want IO actions in your Op monad, so I've already specialized it with the IO monad for you (see the definition of type Op). But monad transformers allow you to stack arbitrary monads. This what intoverflow is talking about. You can read more about them here and here.

查看更多
登录 后发表回答