【问题标题】:Is 'Chaining operations' the "only" thing that the Monad class solves?“链式操作”是 Monad 类解决的“唯一”问题吗?
【发布时间】:2014-02-11 05:48:06
【问题描述】:

为了澄清这个问题:这是关于 monad 类型类的优点(而不是没有统一类的实例)。

在阅读了许多参考资料后(见下文), 我得出的结论是,实际上,monad 类 可以解决一个但重大而关键的问题:具有上下文的类型上的函数“链接”。因此,著名的句子“单子是可编程的分号”。 其实一个monad可以看成an array of functions with helper operations

我坚持monad的区别class,理解为其他类型的通用接口;以及这些实例化类的其他类型(因此,“一元类型”)。

我知道 monad 类本身只能解决运算符的链接,因为主要是它只要求它的类型实例 拥有bind >>=return,并告诉我们他们必须如何表现。作为奖励,编译器极大地帮助了为单子类型提供do 符号的编码。

另一方面, 每个单独的类型实例化了解决每个具体问题的 monad 类,但不仅仅是作为 Monad 的一个实例。例如Maybe 解决“函数如何返回值或错误”,State 解决“如何拥有具有全局状态的函数”,IO 解决“如何与外界交互”等等。所有这些类都在上下文中封装了一个值。

但迟早,我们将需要对此类上下文类型进行链接操作。即,我们需要按特定顺序组织对这些类型的函数的调用(有关此类问题的示例,请阅读You could have invented monads 中有关多值函数的示例)。

如果您将每种类型都作为 monad 类的实例,那么您就解决了链接问题。 要使链接正常工作,您需要 >>= 只需要它具有的确切签名,没有其他的。 (见this question)。

因此,我猜下次您定义上下文数据类型 T 来解决某些问题时,如果您需要对函数的调用(在 T 的值上)进行排序,请考虑将 T 设为 Monad 的实例(如果您需要“选择链接”,如果您可以从do 表示法中受益)。并确保您做对了,检查 T 是否满足 monad laws

然后,我向 Haskell 专家提出两个问题:

  1. 一个具体的问题:单子类是否有任何其他问题可以由 ifself 解决(除了单子类)?如果有的话,那么它与链接操作问题的相关性如何?
  2. 一个可选的一般性问题:我的结论是否正确,我是否误解了什么?

参考文献

教程

StackOverflow 问答

【问题讨论】:

  • 只是为了澄清,你想知道 monad 类型类的优点(而不是没有统一类的实例)或 monad 概念(由 monad 操作和法律定义) ?
  • @ollanta 关于 monad 类型类的优点(与没有统一类的实例相反),
  • 那么我会说它除了样板之外没有解决任何问题,就像所有类型类一样。我认为没有人会说 monad 类型类解决了 X 问题。相反,它具体化了 Haskell 中的一个有用概念,以便我们可以讨论 monad 上的函数(了解它们的可能性和限制),而不是类型上的函数以及实现专门协议的那些类型上的更多函数。一切都是为了让抽象变得美丽。
  • 从给出的 2 个答案中:“monad 类允许其实例类型重用用于链接操作的代码,并使链接操作依赖于前一个操作的结果成为可能”。跨度>

标签: haskell functional-programming monads


【解决方案1】:

你肯定是在用你所说的方式做一些事情——Monad 的意思有很多,你已经很好地把它们分开了。

也就是说,我肯定会说链接操作不是 Monads 解决的主要问题。这可以使用普通的 Functor(有一些麻烦)或使用 Applicatives 轻松解决。 在“选择链接”时,您需要使用完整的 monad 规范。 特别是 ApplicativeMonad 之间的紧张关系来自 Applicative 需要了解侧面的整个结构-静态地影响计算。 Monad 可以在运行时更改该结构,从而牺牲一些可分析性来换取权力。


为了更清楚地说明这一点,您处理 Monad 而不是任何特定 monad 的唯一地方是,如果您定义的东西具有多态性,则限制为 Monad。这在 Control.Monad 模块中反复出现,因此我们可以从那里检查一些示例。

sequence     :: [m a] -> m [a]
forever      :: m a   -> m b
foldM        :: (a -> b -> m a) -> a -> [b] -> m a

立即,我们可以将sequence 排除为特定于Monad,因为在Data.Traversable 中有一个对应的函数,sequenceA 的类型比Applicative f => [f a] -> f [a] 稍微更通用。这应该清楚地表明 Monad 不是唯一的排序方式

同样,我们可以定义foreverA如下

foreverA :: Applicative f => f a -> f b
foreverA f = flip const <$> f <*> foreverA f

因此有更多方法可以对非Monad 类型进行排序。但是我们遇到了foldM的麻烦

foldM             :: (Monad m) => (a -> b -> m a) -> a -> [b] -> m a
foldM _ a []      =  return a
foldM f a (x:xs)  =  f a x >>= \fax -> foldM f fax xs

如果我们尝试将此定义转换为Applicative 样式,我们可能会这样写

foldA             :: (Applicative f) => (a -> b -> f a) -> a -> [b] -> f a
foldA _ a []      =  pure a
foldA f a (x:xs)  = foldA f <$> f a x <*> xs

但是 Haskell 会理所当然地抱怨这不会进行类型检查——每次对 foldA 的递归调用都试图在结果上放置另一个 f 的“层”。使用Monad,我们可以将join 降低这些层,但Applicative 太弱了。


那么这如何转化为Applicatives 限制我们选择运行时?嗯,这正是我们用foldM 表达的,一个单子计算(a -&gt; b -&gt; m a),它依赖于它的a 参数,这是先前单子计算的结果。这种事情在Applicative这个更纯粹的顺序世界中根本没有任何意义。

【讨论】:

  • 总而言之,当“下一个动作取决于前一个动作返回的值”时,您需要一个 monad。当“下一个值取决于前一个值,但生成该值的操作不运行”时,您可以使用应用程序。
  • @jozefg 好吧,您仍然可以使用Fix 来创建您在其他地方解释的一整套嵌套效果。 data SayHi a = SayHi a | Done, io :: Fix SayHi -&gt; IO (); io (Fix Done) = return (); io (Fix SayHi next) = putStrLn "hi" &gt;&gt; io next.
  • @J.Abrahamson 但是Functor 类型类——尤其是它的唯一方法fmap——不允许您将新的“副作用”引入现有的Fix SayHi 值.这就是我认为 jozefg 试图提出的观点。当然,特定的函子可以具有引入新副作用的函数,但这不是重点。
  • 确实如此。我想我一直在思考这个问题,因为我试图明确导致效果排序的确切原因。函子的嵌套是实现这一目标的一种盲目方式,无需任何结构。
【解决方案2】:

要解决单个 monad 类型上的链接操作问题,完全没有必要将其设为 Monad 的实例并确保满足 monad 法则。您可以直接在您的类型上实现链接操作。

它可能与一元绑定非常相似,但不一定完全相同(请记住,列表的绑定是concatMap,一个无论如何都存在的函数,但参数的顺序不同)。而且您不必担心 monad 法则,因为每种类型的界面都会略有不同,因此它们不会有任何共同的要求。

要问Monad 类型类本身 解决了什么问题,请查看对any 中的值起作用的所有函数(在Control.Monad 和其他地方)单子类型。解决的问题是代码重用! Monad 正是所有 monadic 类型的一部分,它们每个人都有。该部分本身就足以编写有用的计算。所有这些函数可以为任何单独的单子类型实现(通常更直接),但它们已经所有单子类型实现了,甚至那些还不存在的。

您不会编写Monad 实例来链接您的类型上的操作(实际上,您通常已经有一种链接方式)。您为 Monad 实例自动附带的所有代码编写了一个 Monad 实例。 Monad 不是解决任何单一类型的任何问题,它是一种将许多不同类型视为单一统一概念实例的方法。

【讨论】:

  • 谢谢。我认为您关于 Monad 类用于“重用允许链接操作的代码”的观点在我的问题中没有明确说明(仅是隐含的),值得强调一下。
猜你喜欢
  • 1970-01-01
  • 1970-01-01
  • 2012-09-18
  • 2021-12-09
  • 2020-05-31
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
相关资源
最近更新 更多