在Haskell中合并monad
我正在尝试编写一个蜘蛛纸牌游戏作为Haskell学习练习。
我的main
功能将为每个游戏调用一次playGame
函数(使用mapM
),传入游戏编号和随机生成器( StdGen
)。 playGame
函数应返回一个Control.Monad.State
monad和一个IO monad,其中包含一个显示游戏画面的String
和一个表示游戏是赢或输的Bool
。
我如何将State
monad与IO
monad结合起来得到返回值? `playGame的类型声明应该是什么?
playGame :: Int -> StdGen a -> State IO (String, Bool)
State IO (String, Bool)
是否正确? 如果不是,它应该是什么?
main
,我计划使用
do
-- get the number of games from the command line (already written)
results <- mapM (game -> playGame game getStdGen) [1..numberOfGames]
这是调用playGame
的正确方法吗?
你想要的是StateT s IO (String, Bool)
,其中StateT
由Control.Monad.State
(来自mtl
包)和Control.Monad.Trans.State
(来自transformers
包)提供。
这种普遍的现象被称为monad变压器,你可以在Monad Transformers中逐步阅读他们的介绍。
有两种方法来定义它们。 其中之一是在使用MonadTrans
类来实现它们的transformers
包中找到的。 第二种方法在mtl
类中找到,并为每个monad使用一个单独的类型类。
transformers
方法的优点是使用单一的类型来实现一切(在这里找到):
class MonadTrans t where
lift :: Monad m => m a -> t m a
lift
有两个很好的属性,任何MonadTrans
实例MonadTrans
必须满足:
(lift .) return = return
(lift .) f >=> (lift .) g = (lift .) (f >=> g)
这些是伪装的函数法则,其中(lift .) = fmap
, return = id
和(>=>) = (.)
。
mtl
类型方法也有其好处,有些东西只能使用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)
这些只是伪装的单子法,其中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
当你开始堆叠多个单体变压器时,这些方法之间会有更多的差异,但我认为现在是一个好的开始。
State
是一个monad, IO
是一个monad。 你试图从头开始写的东西叫做“monad变换器”,而Haskell标准库已经定义了你需要的东西。
查看状态monad变换器StateT
:它有一个参数,它是要包装到State
的内部monad。
每个monad变换器都实现了一组类型类,这样每次变换都会处理它(例如状态转换器只能直接处理与状态有关的函数),或者它将调用传播到内部monad这样当你可以堆叠你想要的所有变压器时,并且有一个统一的接口来访问所有变压器的特性。 这是一种责任链,如果你想以这种方式来看待它。
如果你看看hackage,或者快速搜索堆栈溢出或谷歌,你会发现很多使用StateT
的例子。
编辑 :另一个有趣的阅读是Monad Transformers Explained。
好的,在这里要澄清一些事情:
Monad
类实例的类型构造函数)。 我知道这听起来很迂腐,但它可以帮助你理清事物和事物类型之间的区别,这很重要。 State
做任何事情,所以如果你对如何使用它感到困惑,那么不要觉得你需要! 通常,我只写了我想要的普通函数类型,然后如果我注意到我有很多形如Thing -> (Thing, a)
的函数Thing -> (Thing, a)
我会去“aha,这看起来有点像State
,也许这可能是简化为State Thing a
“。 理解和使用普通函数是使用State
或其朋友的重要的第一步。 IO
是唯一可以完成其工作的东西。 但是, playGame
这个名字并不会立即成为我需要做I / O的事情的名字。 尤其是,如果你只需要(伪)随机数,你可以做到没有IO
。 正如一位评论者指出的那样,MonadRandom非常适合简化这个过程,但是您也可以使用从System.Random
获取并返回StdGen
纯函数。 你只需要确保你的种子( StdGen
)是正确的(自动执行此操作基本上就是为什么State
被发明的原因;你可能会发现你在没有它的情况下尝试编程就会更好地理解它)! 最后,你没有正确使用getStdGen
。 这是一个IO
动作,所以你需要在使用它之前将它的结果与<-
在do
block中绑定(在技术上,你不需要,你有很多选项,但是这几乎肯定是你想要做的)。 像这样的东西:
do
seed <- getStdGen
results <- mapM (game -> playGame game seed) [1..numberOfGames]
这里playGame :: Integer -> StdGen -> IO (String, Bool)
。 但是,请注意,您正在向每个playGame
传递相同的随机种子,这可能是也可能不是您想要的。 如果不是这样,那么当你完成它时,你可以从每个playGame
返回种子,传递给下一个,或者重复使用newStdGen
获得新种子(如果你决定使用newStdGen
,你可以从playGame
做到这一点)保持在IO
)。
无论如何,这并不是一个非常有条理的答案,对此我表示歉意,但我希望它能让你思考。
链接地址: http://www.djcxy.com/p/43357.html