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.
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
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")
If you love us? You can donate to us via Paypal or buy me a coffee so we can maintain and grow! Thank you!
Donate Us With