如何在 IO 中包装 monadic 动作

How to wrap monadic action in IO

我正在尝试将 ReaderT X IO monad 视为 IO 以实现以下目标:

-- 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 例如在间隔中安排另一个 IO 操作。每次打开 Game monad 似乎有点麻烦——而且感觉不对。

我想要实现的是:

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

我的直觉告诉我,这应该是可能的,因为我的 Game monad 知道 IO。有没有办法隐式 convert/unlift Game monad?

如果我的术语不正确,请原谅。

诀窍是将游戏动作定义为类型 class:

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

然后,为 ReaderT State IO 声明一个 GameMonad 的实例 - 使用 ReaderT / IO 操作实现 spawnCreaturemoveCreature;是的,这可能意味着 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' 类型签名将指示函数具有哪些功能:

spawnTenCreatures :: GameMonad m => m ()

在这里,签名告诉你这个函数 执行 GameMonad 操作 - 它不会,比如说,连接到互联网,写入数据库,或启动导弹:)

(事实上,如果您想进一步了解这种风格,google 的技术术语是 "capabilities")

您可以使用的一个抽象是 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))

另一个较少多态的解决方案是使用 mapReaderT.

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