Why Monoid Everywhere?

我在学 Haskell 时遇到最常见的问题就是你会看到某些名词在被你读了几十遍后,即使你去专门看了教程,也依旧没有很好地理解。没错,我说的就是幺半群(monoid)。这篇文章我们来讲一讲为什么幺半群在 Haskell 里也这么常见。

幺半群(monoid)在数学上的定义是:若 SS 是一个集合,\circ 是二元运算 S×SSS\times S \rightarrow S,且满足下面两个条件,那么 S,\langle S, \circ \rangle 被称为一个幺半群。

  1. 结合性 对于 SS 中的任意元素 aabbcc(ab)c=a(bc)(a \circ b) \circ c = a \circ (b \circ c)
  2. 幺元 存在 SS 中的一个元素 ee 使得对于任意 SS 中的元素 aa 等式 ea=aee \circ a = a \circ e 成立。

在数学中,很多集合都属于幺半群。比如自然数集合 N={0,1,2,}\mathbb N = \{0, 1, 2, \cdots\} 和加法运算 ++ 可以构成一个幺半群,正整数集合 N{0}\mathbb N - \{0\} 和乘法运算 ×\times 也可以构成一个幺半群。

回到计算机世界中,很多数据类型也都符合上面的定义。最常见的是数组类型和数组连接(concat)函数,在元素类型为 a 的数组类型中,幺元是 [],我们也可以保证连接函数的结合性。布尔值类型在逻辑或运算下也是幺半群。如果你拿常见数据结构和类型试一下的话,你会发现其实它们的大多数都可以和某个操作构成幺半群。

现在,问题的前半部分解决了——幺半群如此常见的原因是大多数类型确实都可以在某个运算下构成幺半群。后半个问题是:为什么 Monoid 在 Haskell 中如此常见?

答案就是因为幺半群的两个性质可以被很多新的抽象所用,并且很多抽象可以只基于幺半群实现。所以,把一个类型通过 Monoid 实例化后我们可以和其他类型类(type class)结合起来做很多有趣的事,而不用重写任何函数。举个例子,如果你想写一个把 Maybe String 连接起来的函数,如果你初学 Haskell,你可能会手写一个函数,但实际上通过 Haskell 自带的函数就能做到这一点。

mconcat [(Just "Hello, "), Nothing, (Just "world!")]

首先,mconcat 是 Haskell 中 Monoid 自带的一个默认实现,它做的事就是把一组元素通过幺半群的二元运算串联起来;其次,Maybe a 类型实现了 Monoid。所以上面的代码可以直接运行。