我对单子的理解是否正确?

8

所以,我目前正在学习Haskell,并且希望确认或驳斥我对单子的理解。

从阅读CIS194课程中我得出的结论是,单子基本上是用于在自定义集合上定义自定义二元操作的“API”。

然后我去了解更多信息,我遇到了大量非常令人困惑的教程,试图澄清这件事,所以我不太确定了。

我有不错的数学背景,但我被所有比喻搞混了,现在寻求明确的是/否回答我的单子理解。


3
一个单子是一种带有二元结合运算((.):S * S -> S)和该运算的单位元素(id:S)的代数结构(S)。它必须满足结合律(即a.(b.c)=(a.b).c)和单位元法则(id.x=x.id=x)。 - Aadit M Shah
3个回答

8

维基百科中的定义:

在抽象代数学中,单子半群是一种具有单一结合二元运算和一个单位元素的代数结构。

我认为你的理解是正确的。从编程角度来看,Monoid是一个必须实现两个“方法”的接口。

你描述的唯一缺失的部分是“单位元”,没有它你所描述的就是半群

任何具有“零”或“空”以及两个值的组合方法的东西都可以成为Monoid。需要注意的一点是,可能会有多种方式将一个集合/类型变为Monoid,例如通过加法并使用标识符0或乘法并使用标识符1来处理数字。


2
@Reygoch 这些复杂的教程可能存在是因为人们常常害怕数学,所以作者觉得他们需要像对待孩子一样小心翼翼地处理单子,只是因为这个名字很吓人。一个试图不让事情变得神秘的单子解释是维基百科上的解释(免责声明:我是那里的贡献者)。 - duplode
4
任何具有“零”或“空”的特征,并且有一种组合两个值的方式的结构,都可以称之为一个单子(monoid)。这种组合方式必须是可结合的。 - jberryman
@jberryman 从数学角度来看是同意的。但从编程角度来看,我认为更像是“真的,真的应该”以避免出现意外的实现。 - ryachza
1
如果实现方式有意外的情况发生,那么你所拥有的 Monoid 就不是一个单子。 - duplode
1
“Monoid”实例必须遵守单子定律,直到涉及到问题类型的基本等价概念。在大多数情况下(例如队列/序列连接、优先级队列融合或集合并),这不会是结构相等性。然而,如果未能以这种弱化的意义定义适当的单子,将使任何使用您的库的人讨厌您。 - dfeuer
显示剩余4条评论

5

来自沃尔夫拉姆:

一个幺半群是一个集合,它在一个关联的二元操作下封闭,并且具有S中的身份元素I,使得对于所有a在S中,Ia=aI=a。

来自维基:

在抽象代数学中,幺半群是一种具有单个关联二元操作和恒等元素的代数结构。

所以你的直觉或多或少是正确的。

你应该记住的是,它不是为Haskell中的“自定义集”定义的,而是为类型定义的。区别很小(因为类型理论中的类型非常类似于集合理论中的集合),但可以定义Monoid实例的类型不需要表示数学集合的类型。

换句话说:类型描述了属于该类型的所有值的集合。 Monoid是一个“接口”,它声明任何声称遵守该接口的类型必须提供标识值,将两个该类型的值组合的二元操作,并且这些方程式应满足为了使所有通用Monoid操作正常工作(例如通用总和列表中的幺半群值)并且不产生不合逻辑/不一致的结果。

此外,请注意,该集合(类型)中存在一个身份元素是成为Monoid类的实例所必需的。

例如,自然数在加法下形成幺半群(标识= 0):

0 + n = n
n + 0 = n

还有乘法(单位元为1):

1 * n = n
n * 1 = n

同时,列表在 ++ 下也构成一个幺半群(单位元为 []):

[] ++ xs = xs
xs ++ [] = xs

此外,类型为a -> a的函数在组合操作下形成一个幺半群(单位元为id)。

id . f = f
f . id = f

需要记住的是,Monoid 并不是关于代表集合的类型,而是关于将类型视为集合的类型。


作为一个构造不良的 Monoid 实例的示例,请考虑:

import Data.Monoid

newtype MyInt = MyInt Int deriving Show

instance Monoid MyInt where
  mempty = MyInt 0
  mappend (MyInt a) (MyInt b) = MyInt (a * b)

如果你现在尝试对一个 MyInt 值的列表执行 mconcat 操作,你将始终会得到 MyInt 0 作为结果,因为同一元素值 0 和二进制操作 * 不兼容。
λ> mconcat [MyInt 1, MyInt 2]
MyInt 0

3

从基本层面上来说,你是正确的 - 它只是我们用 <> 表示的二元运算符的 API。

然而,单子概念的价值在于它与其他类型和类的关系。文化上,我们已经决定使用 <> 来自然地连接/附加相同类型的两个东西。

考虑以下例子:

{-# LANGUAGE OverloadedStrings #-}

import Data.Monoid

greet x = "Hello, " <> x

greet函数是极其多态的 - x可以是字符串、字节串或文本,只是其中一些可能性。此外,在每种情况下,它基本上都会像您预期的那样 - 它将x附加到字符串“Hello,”。

此外,有许多算法可用于任何可以累积的内容,并且这些算法是泛化为Monoid的良好候选对象。例如,请考虑来自Foldable类的foldMap函数:

foldMap ::  Monoid m => (a -> m) -> t a -> m
foldMap不仅泛化了对结构的折叠,而且可以通过替换正确的Monoid实例来泛化如何执行累积。
如果我有一个包含Ints的可折叠结构t,我可以使用Sum monoid和Product monoid等在foldMap中获得它们的总和或积等。
最后,使用<>提供了方便。例如,有大量不同的Set实现,但对于它们所有的实现,s <> t始终是两个相同类型的集合st的并集。这使我能够编写不关心底层set实现的代码,从而简化我的代码。同样的事情也适用于许多其他数据结构,例如:sequences, trees, maps, priority queues等。

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接