Suppose we had an IO -ify monad State . How will it look like? Our pure State monad is just a new type:
s -> (a, s)
Well, the IO version can fix side effects a bit before returning final values ​​that would look like this:
s -> IO (a, s)
This template is so common that it has a name, in particular StateT :
newtype StateT sma = StateT { runStateT :: s -> m (a, s) }
The name has a T at the end, because it is the T ransformer monad. We call m "base monad" and StateT sm "converted" monad.
StateT sm is only a Monad if m is Monad :
instance (Monad m) => Monad (StateT sm) where {- great exercise -}
However, in addition, all monad transformers implement the MonadTrans class, defined as follows:
class MonadTrans t where lift :: (Monad m) => ma -> tma instance MonadTrans (StateT s) where {- great exercise -}
If T is StateT s , then the lift type specializes in:
lift :: ma -> StateT sma
In other words, this allows us to “raise” an action in the base monad to become an action in the transformed monad.
So, for your specific problem, you need the StateT (IntMap kv) IO monad, which extends the IO an additional State . Then you can write your entire program in this monad:
main = flip runStateT (initialState :: IntMap kv) $ do m <- get
Note that I'm still using get and put . This is because the transformers package implements all the concepts I have described and generalizes the get and put signatures as:
get :: (Monad m) => StateT sms put :: (Monad m) => s -> StateT sm ()
This means that they automatically work inside StateT . transformers , it simply defines State as:
type State s = StateT s Identity
This means that you can use get and put for State and StateT .
To learn more about monad transformers, I highly recommend Monad Transformers - step by step .