Хитрость в том, чтобы определить игровые действия как класс типов:
class Monad m => GameMonad m where
spawnCreature :: Position -> m Creature
moveCreature :: Creature -> Direction -> m ()
Затем объявите экземпляр GameMonad
для ReaderT State IO
- реализация spawnCreature
и moveCreature
с использованием действий ReaderT / IO; да, это, скорее всего, будет означать liftIO
, но только в пределах указанного экземпляра - остальная часть вашего кода сможет без проблем вызывать spawnCreature
и moveCreature
, плюс сигнатуры типа ваших функций будут указывать, какие возможности функции имеет:
spawnTenCreatures :: GameMonad m => m ()
Здесь подпись говорит вам, что эта функция только выполняет операции GameMonad - что она, скажем, не подключается к Интернету, не пишет в базу данных и не запускает ракеты:)
(На самом деле, если вы хотите узнать больше об этом стиле, технический термин для Google - это «возможности»)