Haskell中的单体组合

Haskell中的单体组合,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 -

我想写一个蜘蛛纸牌玩家作为哈斯克尔的学习练习

My
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 transformer
StateT
:它有一个参数,该参数是要包装到
状态的内部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
    ,是唯一能完成其工作的东西。但是,
    playGame
    这个名字并没有立即作为需要进行I/O操作的东西的名字出现在我面前。特别是,如果你只需要(伪)随机数,你可以不用
    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]