Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

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.

like image 570
Markus Rother Avatar asked Jan 26 '23 03:01

Markus Rother


2 Answers

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
like image 80
4castle Avatar answered Jan 31 '23 13:01

4castle


The trick is to define the game actions as a type class:

class Monad m => GameMonad m where
  spawnCreature :: Position -> m Creature
  moveCreature :: Creature -> Direction -> m ()

Then, declare an instance of GameMonad for ReaderT State IO - implementing spawnCreature and moveCreature using ReaderT / IO actions; yes, that will likely imply liftIO's, but only within said instance - the rest of your code will be able to call spawnCreature and moveCreature without complications, plus your functions' type signatures will indicate which capabilities the function has:

spawnTenCreatures :: GameMonad m => m ()

Here, the signature tells you that this function only carries out GameMonad operations - that it doesn't, say, connect to the internet, write to a database, or launch missiles :)

(In fact, if you want to find out more about this style, the technical term to google is "capabilities")

like image 21
typedfern Avatar answered Jan 31 '23 15:01

typedfern