Haskell中的单体组合
我想写一个蜘蛛纸牌玩家作为哈斯克尔的学习练习 MyHaskell中的单体组合,haskell,io,state-monad,Haskell,Io,State Monad,我想写一个蜘蛛纸牌玩家作为哈斯克尔的学习练习 Mymain函数将为每个游戏调用一次playGame函数(使用mapM),传入游戏编号和随机生成器(StdGen)。playGame函数应返回一个Control.Monad.StateMonad和一个IO Monad,其中包含一个String显示游戏表和一个Bool指示游戏是赢还是输 如何将Statemonad与IOmonad结合起来作为返回值?“playGame”的类型声明应该是什么 playGame :: Int -> StdGen a -
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)
状态IO(字符串、布尔值)
正确吗?如果没有,应该是什么
在main
中,我计划使用
do
-- get the number of games from the command line (already written)
results <- mapM (\game -> playGame game getStdGen) [1..numberOfGames]
do
--从命令行获取游戏数(已写入)
结果playGame getStdGen)[1..numberOfGames]
这是调用
playGame
的正确方法吗?State
是单子,而IO
是单子。您试图从头开始编写的内容称为“monad transformer”,Haskell标准库已经定义了您需要的内容
查看state monad transformerStateT
:它有一个参数,该参数是要包装到状态的内部monad
每个monad transformer都实现了一组类型类,因此对于每个实例,transformer每次都可以处理它(例如,状态转换器只能直接处理与状态相关的函数),或者它以这样一种方式将调用传播到内部monad,即当您可以堆叠所需的所有转换器时,并且有一个统一的界面来访问所有这些功能。如果你想这样看的话,这是一种
如果您查看,或在stack overflow或google上快速搜索,您会发现大量使用StateT
的示例
编辑:另一个有趣的阅读是。您想要的是
StateT s IO(String,Bool)
,其中StateT
由Control.Monad.State
(来自mtl
包)和Control.Monad.Trans.State
(来自transformers
包)提供
这种普遍现象被称为单子变压器,您可以在中阅读有关它们的介绍
定义它们有两种方法。其中一个可以在transformers
包中找到,该包使用MonadTrans
类来实现它们。第二种方法是在mtl
类中找到的,它为每个monad使用一个单独的类型类
transformers
方法的优点是使用单个类型类来实现所有内容(发现):
lift
有两个很好的属性,MonadTrans
的任何实例都必须满足:
(lift .) return = return
(lift .) f >=> (lift .) g = (lift .) (f >=> g)
这些是伪装的函子定律,其中(lift.)=fmap
,return=id
和(>=>)=(.)
mtl
类型类方法也有其优点,有些问题只能使用mtl
类型类来解决,但是缺点是每个mtl
类型类都有自己的一套规则,在为其实现实例时必须记住。例如,MonadError
type类(found)定义为:
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=(>>=)
(单子定律是伪装的类别定律,其中return=id
和(>=>)=(。
)
对于您的特定问题,您编写程序的方式将是相同的:
do
-- get the number of games from the command line (already written)
results <- mapM (\game -> playGame game getStdGen) [1..numberOfGames]
当您开始堆叠多个monad transformer时,两种方法之间的差异会变得更加明显,但我认为这是一个良好的开端。好的,这里有几点需要澄清:
- 你不能“退回单子”。monad是一种类型,而不是一种值(准确地说,monad是一种类型构造函数,它具有
类的实例)。我知道这听起来很迂腐,但它可能会帮助你理清头脑中事物和事物类型之间的区别,这一点很重要monad
- 请注意,如果没有它,您无法使用
做任何不可能的事情,因此如果您对如何使用它感到困惑,那么就不必这样做了!通常,我只编写我想要的普通函数类型,然后如果我注意到我有很多函数的形状像State
,我会说“啊哈,这看起来有点像东西->(东西,a)
,也许可以简化为状态
”。理解并使用普通函数是使用状态
或其朋友的重要第一步State
- 另一方面,
,是唯一能完成其工作的东西。但是,IO
这个名字并没有立即作为需要进行I/O操作的东西的名字出现在我面前。特别是,如果你只需要(伪)随机数,你可以不用playGame
来完成。正如一位评论者所指出的,这对于简化此过程非常有用,但您也可以使用纯函数,从IO
获取并返回System.Random
。您只需确保正确地执行种子(StdGen)线程(自动执行此操作基本上就是StdGen
发明的原因;在尝试不使用它编程后,您可能会发现自己更了解它!)State
- 最后,您没有正确使用
。这是一个getStdGen
操作,因此需要将其结果与IO
绑定。但是,请注意,您正在将相同的随机种子传递给每个IO(String,Bool)
,这可能是,也可能不是游戏
do -- get the number of games from the command line (already written) results <- mapM (\game -> playGame game getStdGen) [1..numberOfGames]
-- 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
do seed <- getStdGen results <- mapM (\game -> playGame game seed) [1..numberOfGames]