你想要的是StateT s IO (String, Bool)
,哪里StateT
由Control.Monad.State
(来自mtl
包)和Control.Monad.Trans.State
(来自transformers
包)提供。
这种普遍现象称为 monad 转换器,您可以在Monad Transformers, Step by Step中阅读对它们的精彩介绍。
有两种定义它们的方法。其中之一位于transformers
使用MonadTrans
类来实现它们的包中。第二种方法可以在mtl
类中找到,并为每个 monad 使用单独的类型类。
该transformers
方法的优点是使用单个类型类来实现所有内容(在此处找到):
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
wherereturn = id
和(>=>) = (.)
。
类型类方法也有它的mtl
好处,有些事情只能使用mtl
类型类来彻底解决,但缺点是每个mtl
类型类都有自己的一套法则,在为它实现实例时你必须记住. 例如,MonadError
类型类(在此处找到)定义为:
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 法则,wherethrowError = return
和catchError = (>>=)
(而 monad 法则是伪装的范畴法则,wherereturn = 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
当您开始堆叠多个单子变压器时,这些方法之间的差异会变得更加明显,但我认为现在这是一个好的开始。