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?
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 beIO
.Here's an example of what that might look like:
The key things to observe:
MonadTrans
classlift
function acting ongetLine
, which is used to bring thegetline
function from theIO
monad and into theOp IO
monad.Incidentally, if you don't want the
IO
monad to always be present, you can replace it with theIdentity
monad inControl.Monad.Identity
. TheOp Identity
monad behaves exactly the same as your originalOp
monad.Use liftIO
You're already very close! Your suggestion
is excellent and the way to go.
To be able to execute
getLine
in anOp
context, you need to 'lift' theIO
operation into theOp
monad. You can do this by writing a functionliftIO
:You can now write:
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:
So that your version of
liftIO
becomes an instance ofMonadIO
instead: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 ownMonad
instance, which is always the same for the state monad.StateT
already has aMonad
instance and aMonadIO
instance, so you can use those immediately.Monad transformers
StateT
is a so-called monad transformer. You only wantIO
actions in yourOp
monad, so I've already specialized it with theIO
monad for you (see the definition oftype 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.