在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) ,其中StateTControl.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 .) = fmapreturn = 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 = returncatchError = (>>=) (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”。 monad是一种类型,而不是一种价值(准确地说,monad是一个具有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

    上一篇: Combining monads in Haskell

    下一篇: Haskell Monad Functions