haskellmonadsmonad-transformersreader-monad

How to wrap monadic action in IO


I am trying to treat a ReaderT X IO monad as IO to achieve the following:

-- this is the monad I defined:
type Game = ReaderT State IO                                                                                                            

runGame :: State -> Game a -> IO a                                                                                                      
runGame state a = runReaderT a state                                                                                                    

readState :: Game State                                                                                                                 
readState = ask                                                                                                                         

-- some IO action, i.e. scheduling, looping, etc.                                                                                                                    
ioAction :: IO a -> IO ()
ioAction = undefined

-- this works as expected, but is rather ugly                                                                                                                                       
doStuffInGameMonad :: Game a -> Game ()                                                                                                 
doStuffInGameMonad gameAction = do                                                                                                      
  state <- readState                                                                                                               
  liftIO $ ioAction $ runGame state gameAction

ioAction for example is scheduling another IO action in intervals. Unwrapping the Game monad every time seems a bit cumbersome -- and feels wrong.

What I am trying to achieve instead is:

doStuffInGameMonad :: Game a -> Game ()                                                                                                 
doStuffInGameMonad gameAction = ioAction $ gameAction                                                                                   

My intuition tells me, this should be possible somehow, because my Game monad is aware of IO. Is there a way to implicitly convert/unlift the Game monad?

Please excuse if my terminology is not correct.


Solution

  • One abstraction you can use is the MonadUnliftIO class from the unliftio-core package. You can do it using withRunInIO.

    import Control.Monad.IO.Unlift (MonadUnliftIO(..))
    
    doStuffInGameMonad :: MonadUnliftIO m => m a -> m ()
    doStuffInGameMonad gameAction = withRunInIO (\run -> ioAction (run gameAction))
    

    Another less polymorphic solution would be to use mapReaderT.

    doStuffInGameMonad :: Game a -> Game ()
    doStuffInGameMonad gameAction = mapReaderT ioAction gameAction