【问题标题】:Combining monads in Haskell在 Haskell 中组合单子
【发布时间】:2012-06-06 12:02:30
【问题描述】:

我正在尝试编写一个蜘蛛纸牌播放器作为 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的正确方法吗?

【问题讨论】:

标签: haskell io state-monad


【解决方案1】:

您想要的是StateT s IO (String, Bool),其中StateT 由Control.Monad.State(来自mtl 包)和Control.Monad.Trans.State(来自transformers 包)提供。

这种普遍现象称为单子变换器,您可以在Monad Transformers, Step by Step 中阅读有关它们的精彩介绍。

有两种定义它们的方法。其中之一位于 transformers 包中,该包使用 MonadTrans 类来实现它们。第二种方法可以在 mtl 类中找到,并为每个 monad 使用单独的类型类。

transformers 方法的优点是使用单个类型类来实现所有内容(找到 here):

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 和(&gt;=&gt;) = (.)。

mtl 类型类方法也有它的好处,有些事情只能使用mtl 类型类解决,但缺点是每个mtl 类型类都有自己的为它实施实例时必须记住的一组法律。例如,MonadError 类型类(找到here)定义为:

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 = (&gt;&gt;=)(而 monad 法则是伪装的范畴法则,return = id 和 (&gt;=&gt;) = (.))。

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

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

当您开始堆叠多个 monad 转换器时,这些方法之间的差异会变得更加明显,但我认为现在这是一个好的开始。

【讨论】:

  • 非常好的和完整的答案。谢谢。
  • StateT IO (String, Bool) 不正确——一种不匹配。它是StateT s m a,s 是状态类型,m 是 monad,a 是结果类型。
  • 另外,mtl 方法和transformers 方法实际上并不是做同一件事的不同方式——MonadError 实现了与MonadTrans 不同的目标。并且这些法律并不是伪装的Monad 法律——它们非常相似,但所涉及的事物的类型及其含义是不同的。我的意思是,它们基本上是一个单位律和一个结合律,但是关于完全不同的操作。
  • @benmachine 谢谢,已修复。此外,MonadError 法则实际上只是 monad 法则的类型限制版本,因为 e 通过函数依赖隐藏在 m 中,因此您无法访问它们的全部功能,但您仍然可以使用这些法律来验证您是否正确实现了一个实例。
  • 我不确定你的新类型是他想要什么——s 是干什么用的?关于 MonadError 的东西,我可以理解你的意思,但我不确定这个类比是否像你所说的那样基本——我认为错误类型是由 monad 决定的,而不是用户,是一个重要的区别。我坚持我的评论,mtl 和 transformers 方法在很大程度上解决了不同的问题 - 请注意,现在 mtl 大量导入和使用 transformers,所以暗示这一点很奇怪他们在竞争。
【解决方案2】:

State 是一个单子,IO 是一个单子。您尝试从头开始编写的内容称为“monad 转换器”,Haskell 标准库已经定义了您需要的内容。

看看状态 monad 转换器 StateT:它有一个参数是你想要包装到 State 中的内部 monad。

每个 monad 转换器都实现了一堆类型类,因此对于每个实例,转换器每次都可以处理它(例如,状态转换器只能直接处理与状态相关的函数),或者它将调用传播到内部 monad 的方式是当你可以堆叠所有你想要的转换器时,并且有一个统一的接口来访问所有它们的特性。这是一种chain of responsibility,如果你想这样看的话。

如果你查看hackage,或者快速搜索堆栈溢出或谷歌,你会发现很多StateT 的用法示例。

编辑:另一个有趣的阅读是Monad Transformers Explained。

【讨论】:

  • 我喜欢学习 Haskell 多次重新发现轮子的方式...找到问题的解决方案并发现它是一种通用的设计模式实际上非常好。
  • @FUZxxl:是的,确实如此:)
【解决方案3】:

好的,这里有几件事要澄清:

  • 您不能“返回单子”。 monad 是一种 type,而不是一种 value(准确地说,monad 是一个具有Monad 类实例的 type constructor)。我知道这听起来很迂腐,但它可能会帮助您理清脑海中事物和事物类型之间的区别,这一点很重要。
  • 请注意,您不能使用State 做任何没有它是不可能的事情,所以如果您对如何使用它感到困惑,那就不要觉得有必要!通常,我只写我想要的普通函数类型,然后如果我注意到我有很多类似Thing -&gt; (Thing, a) 的函数,我会说“啊哈,这看起来有点像State,也许这可以简化为@ 987654326@"。理解和使用普通函数是使用 State 或其朋友的重要的第一步。
  • 另一方面,IO 是唯一可以完成其工作的东西。但是playGame 这个名字并没有立即出现在我的脑海中,因为它是需要做 I/O 的东西的名字。特别是,如果您只需要(伪)随机数,则无需IO 也可以这样做。正如评论者所指出的,MonadRandom 非常适合简化此操作,但同样,您可以使用纯函数从 System.Random 获取并返回 StdGen。你只需要确保你正确地穿线你的种子(StdGen)(自动这样做基本上就是为什么State被发明了;你可能会发现在没有它的情况下你会更好地理解它!)
  • 最后,您没有完全正确地使用getStdGen。这是一个IO 操作,因此您需要在使用它之前将其结果与&lt;- 绑定在do-block 中(从技术上讲,您不需要,您有很多选择,但这几乎肯定是你想要做的)。像这样的:

    do
      seed <- getStdGen
      results <- mapM (\game -> playGame game seed) [1..numberOfGames]
    

    这里playGame :: Integer -&gt; StdGen -&gt; IO (String, Bool)。但是请注意,您将 same 随机种子传递给每个 playGame,这可能是也可能不是您想要的。如果不是,那么您可以在完成后从每个 playGame 返回种子,传递给下一个,或者使用 newStdGen 重复获取新种子(您可以从内部 @987654344 @,如果您决定将其保留在 IO)。

无论如何,这不是一个非常结构化的答案,对此我深表歉意,但我希望它能给你一些思考。

【讨论】:

  • "monad 是一种类型,不是一种值。" 说它实际上是类型构造函数?
  • 是的,monad 是一种类型构造函数,但我不想听起来太专业——我只是想强调“monad 属于类型的世界”。我将对其进行更精确的编辑。
猜你喜欢
  • 2010-11-10
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
  • 2016-08-18
  • 2011-10-07
  • 1970-01-01
  • 2014-05-30
相关资源
最近更新 更多