3

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.

Mark Seemann
  • 225,310
  • 48
  • 427
  • 736
Markus Rother
  • 414
  • 3
  • 10
  • 2
    No, you cannot implicitly unlift. It is always explicit. There are libraries to help, but under the hood they do exactly what you did here. – Daniel Wagner Jun 25 '19 at 13:26

2 Answers2

4

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
4castle
  • 32,613
  • 11
  • 69
  • 106
  • Exactly what I was looking for! I will go with `mapReaderT`. What I learned once more: In Haskell when you can come up with the signature of the function your are looking for, you almost solved your problem. Thanks everybody! – Markus Rother Jun 26 '19 at 07:02
2

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")

typedfern
  • 1,257
  • 1
  • 6
  • 14
  • This doesn't appear to actually address the question to me. In particular, once I have my `ReaderT State IO` instance, how do I use this new infrastructure that you recommend to make the implementation of `doStuffInGameMonad` less cumbersome? – Daniel Wagner Jun 25 '19 at 13:26
  • As far as I understand (from an OOP background), what you (@typedfern) suggest is information hiding by using a type class. In my case that is not interesting, because the `ioAction` I referred to, should NOT belong to the same context as the Game Monad. – Markus Rother Jun 25 '19 at 14:20
  • @MarkusRother, you can create multiple monads for different purposes: say, MonadGameState, MonadNetwork, etc; then, if you have a function that both changes the game state and accesses the network, you set two constraints: ```myFunction :: (MonadGameState m, MonadNetwork m) => m ()```. You can place the ioAction in question within whatever monad is appropriate, having your different effects grouped into different classes. – typedfern Jun 25 '19 at 15:02