如何将两个MonadState实例合并到同一Monad?井字棋测试场景解惑
我正在实现一个TicTacToe(井字棋)游戏服务器,为方便测试定义了TTTMonad类:
class (Monad m, MonadState TTTState m) => TTTMonad m where getMove :: m Move
生产环境中getMove是等待用户输入的IO动作,但测试时我希望用预定义的Move列表重放特定对局。
我的初始方案是创建包含待重放Move列表和TTTState的元组状态:
newtype ReplayMonad a = ReplayMonad { unReplay :: State ([Move], TTTState) a }
但因函数依赖,无法为该Monad同时实现MonadState [Move]和MonadState TTTState实例。我原本想这样实现TTTMonad:
instance TTTMonad ReplayMonad where -- 消耗列表中的第一个Move并返回 getMove = do m:ms <- gets fst modify (\(_, gs) -> (ms, gs)) -- 更新状态为剩余Move列表+当前游戏状态 return m
现有代码仅基于MonadState TTTState参数化,我仅需新增状态以在Monad上下文内获取Move。请问我哪里出错了?处理此类简单Monad扩展的常用模式是什么?
问题分析
你核心的问题在于**TTTMonad的约束要求实例必须实现MonadState TTTState**,但你的ReplayMonad的状态是元组([Move], TTTState),它默认的MonadState实例对应的是这个元组类型,而非单独的TTTState,因此不满足TTTMonad的约束条件,自然无法通过编译。
解决方法与常用模式
1. 为组合状态实现目标MonadState实例
不需要同时实现两个MonadState实例,而是为ReplayMonad实现针对TTTState的MonadState实例,通过投影元组的对应部分来完成游戏状态的读取和修改,同时在内部处理Move列表的状态:
首先补全ReplayMonad的基础Monad实例,再实现MonadState TTTState:
import Control.Monad.State instance Functor ReplayMonad where fmap f (ReplayMonad ma) = ReplayMonad $ fmap f ma instance Applicative ReplayMonad where pure = ReplayMonad . pure (ReplayMonad mf) <*> (ReplayMonad ma) = ReplayMonad $ mf <*> ma instance Monad ReplayMonad where return = ReplayMonad . return (ReplayMonad ma) >>= f = ReplayMonad $ ma >>= unReplay . f -- 实现MonadState TTTState约束,仅操作元组中的第二部分(游戏状态) instance MonadState TTTState ReplayMonad where get = ReplayMonad $ gets snd modify updateGame = ReplayMonad $ modify (\(moves, game) -> (moves, updateGame game))
之后就可以正确实现TTTMonad实例,直接操作元组状态中的Move列表:
instance TTTMonad ReplayMonad where getMove = ReplayMonad $ do (m:remainingMoves, gameState) <- get put (remainingMoves, gameState) return m
2. 使用Monad Transformer叠加状态
如果后续需要扩展更多效果(比如日志、异常),更优雅的方式是用Monad Transformer叠加状态。例如把Move列表的状态作为外层的StateT,包裹内层的State TTTState:
newtype ReplayMonad a = ReplayMonad { unReplay :: StateT [Move] (State TTTState) a } deriving (Functor, Applicative, Monad, MonadState [Move]) -- 手动实现针对TTTState的MonadState实例,通过lift操作内层状态 instance MonadState TTTState ReplayMonad where get = ReplayMonad $ lift get modify f = ReplayMonad $ lift (modify f) instance TTTMonad ReplayMonad where getMove = do m:ms <- get put ms return m
这种模式的优势在于可以灵活叠加不同的monad效果,比如生产环境可以用IO作为基础monad,测试用纯状态monad。
3. 调整TTTMonad定义(解除MonadState依赖)
如果不想受MonadState的约束限制,可以直接在TTTMonad中定义游戏状态的操作方法,而非依赖类型类:
class Monad m => TTTMonad m where getMove :: m Move getGameState :: m TTTState modifyGameState :: (TTTState -> TTTState) -> m ()
这样不同的实例可以自由选择状态存储方式:生产环境用IO结合IORef,测试环境用纯状态元组,完全不需要依赖MonadState类型类,灵活性更高。
内容的提问来源于stack exchange,提问作者tonicebrian

