Monoid 是什么?
我们先看两组例子:
1 | // string concatenation |
我们可以发现,这两组操作其实有着相同的模式:
- 有一个“零值”,记作
zero
,例子中分别是空串
和0
- 有一个二元操作符,记作
op
,例子中分别是concat
和+
op
满足结合律(associativity),即op(x, op(y, z)) == op(op(x, y), z)
- 零值是单位元,即
op(zero, x) == op(x, zero) == x
那么我们可以这样表示monoid:
1 | trait Monoid[A] { |
事实上,monoid是十分普遍的,比如:
- 乘法,单位元是1
- 布尔运算
&&
,单位元是true
- 求
Integer
的最大值max
,单位元是Integer.MIN_VALUE
List
的连接,单位元是Nil(空列表)
- 自函数(参数与返回值的类型相同,即类型为
A => A
的函数)的组合compose
,单位元是id
第5个并不是那么显而易见,实现如下:
1 | def endoMonoid[A]: Monoid[A => A] = new Monoid[A => A] { |
那么monoid是什么?
一个可结合的二元操作符和一个单位元元素,构成一个monoid。
Monoid 有什么用?
Monoid是一种抽象,我们总是可以通过抽象来写出更加通用的代码:事实上,我们可以不关心monoid里的具体类型,直接写出可以对任何monoid都有效的代码。
计算的灵活性
事实上,对于monoid,我们并不关心计算发生的顺序,结合律保证了结果的一致:
1 | // 1. foldRight |
我们可以用任何顺序进行计算,甚至直接将每个任务分派给不同节点开始并行计算!
List的折叠
仔细观察,你会发现当我们对一个List
进行fold
操作(aka reduce
in JavaScript/Python)时,传给fold的操作,恰好是monoid的op
和zero
:
1 | List(1,2,3,4,5).foldLeft(0)((x, y) => x + y) == |
所以可以写一个更加通用的函数,接收一个List[A]
和一个Monoid[A]
,并用这个monoid去折叠:
1 | def concatenate[A](as: List[A], m: Monoid[A]): A = |
甚至可以将List[A]
map 成List[B]
:
1 | def foldMap[A, B](as: List[A], m: Monoid[B])(f: A => B): B = |
通用折叠
事实上,对于所有的可折叠数据结构,我们都可以忽略其具体结构,直接用monoid进行折叠。
比如,我们有一个存放了Int
的可折叠结构,计算其总和:
1 | ints.foldRight(intAddtionMonoid.zero)(intAddtionMonoid.op) |
我们并不关心,这个ints,究竟是List
还是Array
还是Vector
,甚至可能是Tree
或者Stream
!
对于这些结构,我们给它起个名字,叫做Foldable
,我们可以抽象成这样:
1 | trait Foldable[F[_]] { |
PS:F[_]
代表一个F是一个类型构造器,接收一个类型参数。比如,F[A]
可以具象成List[Int]
,Stream[String]
等等。
Monoid的组合
若类型A
和B
是monoid,那么tuple类型(A, B)
同样也是monoid。
我们只需要将A和B的op
和zero
组合成tuple
就可以了。
1 | // am: Monoid[A], bm: Monoid[B] |
一些Monoid的例子
There are monoids for all the following:
· approximate sets such as the Bloom filter;
· set cardinality estimators, such as the HyperLogLog algorithm;
· vectors and vector operations like stochastic gradient descent;
· quantile estimators such as the t-digest
to name but a few.– Chapter 9, Scala with Cats
闲谈
- Monoid可以算作FP中最简单的纯代数(purely algebraic)结构。
- 这种抽象的意义在于——和编程中的任何抽象一样——消除重复代码:抽出通用的结构,表达为抽象的类型和接口,从而消除重复。另外,这种抽象还能降低沟通成本。
- Monoid中文名称叫做幺半群,十分拗口,所以全文都没有出现这个名字。
- Monoid名称出自于抽象代数,我们也完全可以叫作AssociateWithIdentity,对我们编程也是毫无影响的。