我正在尝试将Spider Solitaire播放器编写为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
的正确方法吗?
答案 0 :(得分:12)
您想要的是StateT s IO (String, Bool)
,其中StateT
(来自Control.Monad.State
包)和mtl
(来自{{1}包)。
这种一般现象称为monad变换器,您可以在Monad Transformers, Step by Step中阅读它们的精彩介绍。
定义它们有两种方法。其中一个在Control.Monad.Trans.State
包中找到,它使用transformers
类来实现它们。第二种方法可以在transformers
类中找到,并为每个monad使用单独的类型。
MonadTrans
方法的优点是使用单个类类来实现所有内容(找到here):
mtl
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
。
return = id
类型类方法也有它的好处,有些东西只能使用(>=>) = (.)
类型类来干净地解决,但是缺点是每个mtl
type-class有自己的一套法则,在为它实现实例时你必须记住这些法则。例如,mtl
类型类(找到here)定义为:
mtl
这门课也有法律规定:
MonadError
这些只是伪装的monad法律,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 = (>>=)
)。
对于您的具体问题,编写程序的方式是相同的:
return = id
...但是当你编写(>=>) = (.)
函数时,它看起来像是:
do
-- get the number of games from the command line (already written)
results <- mapM (\game -> playGame game getStdGen) [1..numberOfGames]
当你开始堆叠多个monad变换器时,这些方法之间的差异会更明显,但我认为现在这是一个好的开始。
答案 1 :(得分:8)
State
是monad,IO
是monad。您尝试从头开始编写的内容称为“monad转换器”,Haskell标准库已经定义了您需要的内容。
看一下状态monad变换器StateT
:它有一个参数,它是你想要包裹在State
中的内部monad。
每个monad变换器实现了一堆类型类,这样对于每个实例,变换器每次都可以处理它(例如状态变换器只能直接处理与状态相关的函数),或者它将调用传播到内部monad以这样的方式,当你可以堆叠你想要的所有变换器,并有一个统一的界面来访问所有这些变换器的功能。如果你想以这种方式看待它,那就是chain of responsibility。
如果您查看hackage,或快速搜索堆栈溢出或谷歌,您会发现很多StateT
的使用示例。
编辑:另一个有趣的读物是Monad Transformers Explained。
答案 2 :(得分:2)
好的,这里要清理一些事情:
Monad
类的实例)。我知道这听起来很迂腐,但它可以帮助你理清事物和事物类型之间的区别,这很重要。State
做任何事情,所以如果你对如何使用它感到困惑,那就不要觉得你需要了!通常,我只是编写我想要的普通函数类型,然后如果我注意到我有很多函数形状像Thing -> (Thing, a)
我会去“啊哈,这看起来有点像State
,也许这可以简化为State Thing a
“。理解和使用普通功能是使用State
或其朋友的重要第一步。IO
是唯一可以完成其工作的东西。但名称playGame
并没有立即出现在我身上,而是需要做I / O的事情的名称。特别是,如果仅需要(伪)随机数,则可以在没有IO
的情况下执行此操作。正如评论者指出的那样,MonadRandom非常适合简化这一过程,但您可以再次使用从StdGen
获取并返回System.Random
的纯函数。你必须确保正确地对你的种子(StdGen
)进行线程化(自动执行此操作基本上就是State
被发明的原因;你可能会发现在没有它的情况下尝试编程后你会更好地理解它!)< / LI>
最后,您还没有正确使用getStdGen
。这是IO
操作,因此您需要在使用<-
块之前将其结果与do
绑定(从技术上讲,您不需要 ,你有很多选择,但这几乎可以肯定你想要做的事情)。像这样:
do
seed <- getStdGen
results <- mapM (\game -> playGame game seed) [1..numberOfGames]
此处playGame :: Integer -> StdGen -> IO (String, Bool)
。但请注意,您将相同的随机种子传递给每个playGame
,这可能是您想要的,也可能不是。如果不是,那么,当你完成它时,你可以从每个playGame
返回种子,传递给下一个种子,或者用newStdGen
重复获得新种子(你可以做到)从playGame
内部开始,如果您决定将其保留在IO
中。
无论如何,这不是一个非常有条理的答案,我为此道歉,但我希望它给你一些思考。