28

monad 是一种在(纯)函数式编程中大量使用的数学结构,基本上是 Haskell。但是,还有许多其他可用的数学结构,例如应用函子、强单子或幺半群。有些更具体,有些更通用。然而,单子更受欢迎。这是为什么?

我想出的一个解释是,它们是通用性和特异性之间的最佳平衡点。这意味着 monads 捕获了关于数据的足够假设以应用我们通常使用的算法,并且我们通常拥有的数据满足 monadic 定律。

另一种解释可能是 Haskell 为 monad(do-notation)提供语法,但不为其他结构提供语法,这意味着 Haskell 程序员(以及函数式编程研究人员)直观地被 monad 吸引,其中更通用或特定(高效)的函数会也可以工作。

4

6 回答 6

29

我怀疑对这一特定类型类别 ( Monad) 的过度关注主要是历史上的侥幸。人们经常与 联系IO在一起Monad,尽管这两者是独立有用的想法(如列表反转和香蕉)。因为IO很神奇(有实现但没有外延)并且Monad经常与 . 相关联IO,所以很容易陷入对Monad.

(除此之外:IOeven 是否是单子是值得怀疑的。单子定律是否成立?这些定律对 甚至意味着什么IO,即平等意味着什么?请注意与状态单子的有问题的关联。)

于 2013-05-08T15:40:28.563 回答
17

如果一个类型m :: * -> *有一个Monad实例,你会得到带有 type 的图灵完备的函数组合a -> m b。这是一个非常有用的属性。您可以从特定含义中抽象出各种图灵完备的控制流。它是一种最小的组合模式,支持抽象任何控制流以使用支持它的类型。

Applicative例如,将此与 进行比较。在那里,您只能获得计算能力相当于下推自动机的合成模式。当然,确实更多类型支持组合功能更有限。确实,当您限制可用功率时,您可以进行额外的优化。这两个原因是Applicative该类存在并且有用的原因。但是可以作为实例的事物Monad通常是,以便该类型的用户可以对该类型执行最一般的操作。

编辑: 根据大众的需求,这里有一些使用Monad该类的功能:

ifM :: Monad m => m Bool -> m a -> m a -> m a
ifM c x y = c >>= \z -> if z then x else y

whileM :: Monad m => (a -> m Bool) -> (a -> m a) -> a -> m a
whileM p step x = ifM (p x) (step x >>= whileM p step) (return x)

(*&&) :: Monad m => m Bool -> m Bool -> m Bool
x *&& y = ifM x y (return False)

(*||) :: Monad m => m Bool -> m Bool -> m Bool
x *|| y = ifM x (return True) y

notM :: Monad m => m Bool -> m Bool
notM x = x >>= return . not

将它们与 do 语法(或原始>>=运算符)结合起来,可以为您提供名称绑定、无限循环和完整的布尔逻辑。这是一组众所周知的原语,足以赋予图灵完整性。请注意所有函数是如何被提升为处理一元值而不是简单值的。所有单子效果仅在必要时绑定 - 只有所选分支的效果ifM才绑定到其最终值。如果可能,两者都*&&忽略*||他们的第二个论点。等等..

现在,这些类型签名可能不涉及每个一元操作数的函数,但这只是认知上的简化。如果所有非函数参数和结果都更改为() -> m a. 优化认知开销对用户来说更友好。

现在,让我们看看这些功能与Applicative接口发生了什么。

ifA :: Applicative f => f Bool -> f a -> f a -> f a
ifA c x y = (\c' x' y' -> if c' then x' else y') <$> c <*> x <*> y

嗯,嗯。它具有相同的类型签名。但是这里已经有一个非常大的问题了。x 和 y 的效果都绑定到组合结构中,无论选择哪个值。

whileA :: Applicative f => (a -> f Bool) -> (a -> f a) -> a -> f a
whileA p step x = ifA (p x) (whileA p step <$> step x) (pure x)

好吧,好吧,这似乎没问题,除了它是一个无限循环,因为它ifA总是会执行两个分支......除了它甚至没有那么接近。pure x有类型f awhileA p step <$> step x有类型f (f a)。这甚至不是一个无限循环。这是一个编译错误。让我们再试一次..

whileA :: Applicative f => (a -> f Bool) -> (a -> f a) -> a -> f a
whileA p step x = ifA (p x) (whileA p step <*> step x) (pure x)

拍得好。甚至不要走那么远。whileA p step有类型a -> f a。如果您尝试将它用作 的第一个参数<*>,它会获取顶级类型构造函数的Applicative实例,即不是。是的,这也行不通。(->)f

事实上,我的Monad示例中唯一可以使用该Applicative接口的函数是notM. Functor事实上,该特定功能只需一个接口即可正常工作。其余的部分?他们失败了。

当然,可以预期您可以使用Monad接口编写代码,而您不能使用接口编写代码Applicative。毕竟,它严格来说更强大。但有趣的是你失去了什么。您将失去组合函数的能力,这些函数会根据输入来改变它们的效果。也就是说,你失去了编写某些控制流模式的能力,这些模式组成了带有 types 的函数a -> f b

图灵完备的组合正是Monad界面变得有趣的原因。如果它不允许图灵完备的组合,那么程序员就不可能在IO没有为你很好地预先打包的任何特定控制流中组合动作。事实上,您可以使用Monad原语来表达任何控制流,这使得该IO类型成为在 Haskell 中管理 IO 问题的可行方法。

更多的类型不仅仅是IO语义上有效的Monad接口。碰巧的是,Haskell 具有对整个接口进行抽象的语言工具。由于这些因素,Monad它是一个有价值的类,可以在可能的情况下为其提供实例。这样做可以让您访问为处理一元类型而提供的所有现有抽象功能,而不管具体类型是什么。

因此,如果 Haskell 程序员似乎总是关心Monad一个类型的实例,那是因为它是可以提供的最通用的实例。

于 2013-05-08T18:17:32.193 回答
12

首先,我认为单子比其他任何东西都更受欢迎的说法并不完全正确。Functor 和 Monoid 都有许多不是 monad 的实例。但它们都非常具体。Functor 提供映射,Monoid 连接。Applicative 是我能想到的一类,鉴于其强大的功能,它可能未被充分利用,主要是因为它是该语言相对较新的添加。

但是,是的,单子非常受欢迎。其中一部分是 do 表示法;许多 Monoids 提供 Monad 实例,这些实例仅将值附加到正在运行的累加器(本质上是隐式写入器)。blaze-html 库就是一个很好的例子。我认为原因是类型签名的力量(>>=) :: Monad m => m a -> (a -> m b) -> m b. 虽然 fmap 和 mappend 很有用,但它们能做的却相当有限。然而,bind 可以表达各种各样的东西。当然,它在 IO monad 中被规范化,这可能是在流和 FRP 之前最好的纯函数式 IO 方法(并且在它们旁边对于简单的任务和定义组件仍然有用)。但它也提供了隐式状态(Reader/Writer/ST),可以避免一些非常繁琐的变量传递。各种状态单子尤其重要,因为它们保证状态是单线程的,在融合之前允许纯(非 IO)代码中的可变结构。但是 bind 有一些更奇特的用途,例如扁平化嵌套数据结构(List 和 Set monad),它们在它们的位置上都非常有用(我通常看到它们被脱糖使用,显式调用 liftM 或 (>>=),所以这不是 do 表示法的问题)。因此,虽然 Functor 和 Monoid(以及更罕见的 Foldable、Alternative、Traversable 等)为相当简单的函数提供了标准化接口,但 Monad 的绑定具有更大的灵活性。

总之,我认为你所有的理由都有一定的作用;monad 的流行是由于历史偶然性(do 表示法和 Applicative 的后期定义)以及它们的力量和普遍性(相对于函子、幺半群等)和可理解性(相对于箭头)的结合。

于 2013-05-08T15:26:57.373 回答
7

好吧,首先让我解释一下 monad 的作用是什么:Monad 非常强大,但在某种意义上:你几乎可以使用 monad 来表达任何东西。Haskell 作为一种语言没有动作循环、异常、突变、goto 等。Monad 可以在语言中表达(因此它们并不特殊)并使所有这些都可以访问。

这有积极的一面和消极的一面:积极的一面是,你可以表达你从命令式编程中知道的所有控制结构,以及你不知道的一大堆控制结构。我最近刚刚开发了一个 monad,它可以让你在中间的某个地方重新输入一个计算,并稍微改变一下上下文。这样你就可以运行一个计算,如果它失败了,你只需稍微调整一下值再试一次。此外,一元动作是一流的,这就是您构建循环或异常处理等事物的方式。虽然while在 Haskell 的 C 中是原始的,但它实际上只是一个常规函数

不利的一面是 monads 几乎没有给你任何保证。它们是如此强大,以至于您可以为所欲为,简单地说。换句话说,就像你从命令式语言中知道的那样,仅仅通过查看代码很难推断代码。

更一般的抽象在某种意义上更一般,因为它们允许表达一些你不能表达为单子的概念。但这只是故事的一部分。即使对于 monad,您也可以使用一种称为applicative style的样式,在这种样式中,您可以使用 applicative 接口从小的孤立部分组成您的程序。这样做的好处是您可以通过查看代码来推断代码,并且您可以开发组件而无需关注系统的其余部分。

于 2013-05-08T11:35:16.170 回答
2

单子有什么特别之处?

monadic 接口main在 Haskell 中声名鹊起是因为它取代了原始且笨拙的基于对话的 I/O机制。

至于他们在正式调查环境中的地位……这只是一个看似循环的努力的迭代,现在(2021 年 10 月)大约有半个世纪的历史:

在 1960 年代,一些研究人员开始致力于证明有关程序的事情。努力证明:

  • 一个程序是正确的。

  • 当给定相同的输入时,具有不同代码的两个程序计算出相同的答案。

  • 一个程序比另一个程序快。

  • 给定的程序将始终终止。

虽然这些是抽象的目标,但它们实际上都与“调试程序”的实际目标相同。

这项工作出现了几个难题。一个是规范的问题:在证明一个程序是正确的之前,必须正式且明确地指明“正确”的含义。开发了用于指定程序含义的正式系统,它们 看起来很像编程语言。

编程语言剖析,Alice E. Fischer 和 Frances S. Grodzinsky。

(我强调。)

...回到“编程语言”——除了少数几门——绝对是势在必行的时候。

有人将这个谜题提升到千年问题的级别吗?解决它肯定会以一种或另一种方式推进计算科学和软件工程......

于 2021-10-09T11:58:53.487 回答
0

Monad 之所以特别是因为do符号,它使您可以用函数式语言编写命令式程序。Monad 是一种抽象,它允许您将来自较小的、可重用的组件(它们本身就是命令式程序)的命令式程序拼接在一起。Monad 转换器很特别,因为它们代表了用新特性增强命令式语言。

于 2013-05-17T04:47:32.260 回答