您想要的是StateT s IO (String, Bool),其中StateT 由Control.Monad.State(来自mtl 包)和Control.Monad.Trans.State(来自transformers 包)提供。
这种普遍现象称为单子变换器,您可以在Monad Transformers, Step by Step 中阅读有关它们的精彩介绍。
有两种定义它们的方法。其中之一位于 transformers 包中,该包使用 MonadTrans 类来实现它们。第二种方法可以在 mtl 类中找到,并为每个 monad 使用单独的类型类。
transformers 方法的优点是使用单个类型类来实现所有内容(找到 here):
class MonadTrans t where
lift :: Monad m => m a -> t m a
lift 有两个很好的属性,MonadTrans 的任何实例都必须满足:
(lift .) return = return
(lift .) f >=> (lift .) g = (lift .) (f >=> g)
这些是伪装的函子定律,其中(lift .) = fmap、return = id 和(>=>) = (.)。
mtl 类型类方法也有它的好处,有些事情只能使用mtl 类型类解决,但缺点是每个mtl 类型类都有自己的为它实施实例时必须记住的一组法律。例如,MonadError 类型类(找到here)定义为:
class Monad m => MonadError e m | m -> e where
throwError :: e -> m a
catchError :: m a -> (e -> m a) -> m a
这个类也有法律:
m `catchError` throwError = m
(throwError e) `catchError` f = f e
(m `catchError` f) `catchError` g = m `catchError` (\e -> f e `catchError` g)
这些只是伪装的 monad 法则,throwError = return 和 catchError = (>>=)(而 monad 法则是伪装的范畴法则,return = id 和 (>=>) = (.))。
对于您的具体问题,您编写程序的方式将是相同的:
do
-- get the number of games from the command line (already written)
results <- mapM (\game -> playGame game getStdGen) [1..numberOfGames]
...但是当您编写 playGame 函数时,它看起来像:
-- transformers approach :: (Num s) => StateT s IO ()
do x <- get
y <- lift $ someIOAction
put $ x + y
-- mtl approach :: (Num s, MonadState s m, MonadIO m) => m ()
do x <- get
y <- liftIO $ someIOAction
put $ x + y
当您开始堆叠多个 monad 转换器时,这些方法之间的差异会变得更加明显,但我认为现在这是一个好的开始。