在Haskell中组合monad

时间:2012-06-06 12:02:30

标签: haskell io state-monad

我正在尝试将Spider Solitaire播放器编写为Haskell学习练习。

我的main函数会为每个游戏调用一次playGame函数(使用mapM),传入游戏编号和随机生成器(StdGen)。 playGame函数应返回Control.Monad.State monad和IO monad,其中包含显示游戏画面的StringBool,表示游戏是赢还是输。

如何将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的正确方法吗?

3 个答案:

答案 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 = returncatchError = (>>=))。

对于您的具体问题,编写程序的方式是相同的:

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是一种类型,而不是一种值(确切地说,monad是一个类型的构造函数,它有一个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中。

无论如何,这不是一个非常有条理的答案,我为此道歉,但我希望它给你一些思考。