Scala with Cats 学习笔记3-Monoids和Semigroups

Abstract: 继续阅读 Scala with Cats, 开始学习 MonoidsSemigroups,其中文翻译分别为 幺半群(还有翻译成 独异点) 和 半群

数学背景

MonoidsSemigroups 实际上是严格的数学定义。

Group 群

G为非空集合,如果在G上定义的二元运算*,满足

  • (1)封闭性(Closure):对于任意 $a,b∈G$,有 $a \times b∈G$
  • (2)结合律(Associativity):对于任意 $a,b,c∈G$,有 $(a \times b) \times c=a \times (b \times c)$.
  • (3)幺元(Identity):存在幺元 $e$,使得对于任意 $a∈G,e \times a=a \times e=a$
  • (4)逆元:对于任意 $a∈G$,存在逆元 $a^{-1}$,使得 $a^{-1} \times a=a \times a^{-1}=e$

则称 $(G,\times)$ 是群,简称 $G$ 是群。

SemiGroup 半群

如果仅满足封闭性和结合律,则称G是一个半群(Semigroup);

Monoid 幺半群

如果仅满足封闭性、结合律并且有幺元,则称G是一个含幺半群(Monoid)。

举例

本书为了让读者理解这个数学概念举了很多例子,比如(整数,+) 就满足 封闭性,结合律和幺元的特性。

1
2
3
4
5
6
7
8
// 封闭性
2 + 1 // res0: Int = 3
// 幺元(0)
2 + 0 // res1: Int = 2
0 + 2 // res2: Int = 2
// 结合律
(1 + 2) + 3 // res3: Int = 6
1 + (2 + 3) // res4: Int = 6

(String 类型,拼接操作 ++)也满足这些特性:

1
2
3
4
5
6
7
8
// 封闭性
"One" ++ "two" // res9: String = Onetwo
// 幺元 (空字符串)
"" ++ "Hello" // res10: String = Hello
"Hello" ++ "" // res11: String = Hello
// 结合律
("One" ++ "Two") ++ "Three" // res12: String = OneTwoThree
"One" ++ ("Two" ++ "Three") // res13: String = OneTwoThree

Scala 精简定义

(建议阅读本书时先 Semigroup 再看 Monoid)
首先我们为了从程序角度理解这两个概念,我们可以通过 Scala 来描述一下这两个概念:

1
2
3
4
5
6
7
trait Semigroup[A] {
def combine(x: A, y: A): A
}

trait Monoid[A] extends Semigroup[A] {
def empty: A
}

这个只是接口,具体的类型和二元操作需要自行根据实际场景定义。比如整型和加法:

1
2
3
object IntSemiGroup extends Semigroup[Int] {
def combine(a: Int, b: Int): Int = a + b
}

理论上我们需要验证我们的实现是不是符合半群的要求,比如减法并不符合结合律。

1
2
3
4
5
6
7
8
def associativeLaw[B](x: B, y: B, z: B)(implicit m: Monoid[B]): Boolean = {
m.combine(x, m.combine(y, z)) ==
m.combine(m.combine(x, y), z)
}
def identityLaw[B](x: B)(implicit m: Monoid[B]): Boolean = {
(m.combine(x, m.empty) == x) &&
(m.combine(m.empty, x) == x)
}

Boolean 案例

我们可以定义4个 Monoid: And,Or,Either,XNOR(异或非)

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
implicit val booleanAndMonoid: Monoid[Boolean] =
new Monoid[Boolean] {
def combine(a: Boolean, b: Boolean) = a && b
def empty = true
}
implicit val booleanOrMonoid: Monoid[Boolean] =
new Monoid[Boolean] {
def combine(a: Boolean, b: Boolean) = a || b
def empty = false
}
implicit val booleanEitherMonoid: Monoid[Boolean] =
new Monoid[Boolean] {
def combine(a: Boolean, b: Boolean) =
(a && !b) || (!a && b)

def empty = false
}
implicit val booleanXnorMonoid: Monoid[Boolean] =
new Monoid[Boolean] {
def combine(a: Boolean, b: Boolean) =
(!a || b) && (a || !b)

def empty = true
}

为什么没有 异或 XOR?

Set 案例

Union: Monoid
Intersection:Semigroup
difference: None
symmetric difference: Monoid

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
implicit def setUnionMonoid[A]: Monoid[Set[A]] =
new Monoid[Set[A]] {
def combine(a: Set[A], b: Set[A]) = a union b
def empty = Set.empty[A]
}

implicit def setIntersectionSemigroup[A]: Semigroup[Set[A]] =
new Semigroup[Set[A]] {
def combine(a: Set[A], b: Set[A]) =
a intersect b
}

implicit def symDiffMonoid[A]: Monoid[Set[A]] =
new Monoid[Set[A]] {
def combine(a: Set[A], b: Set[A]): Set[A] =
(a diff b) union (b diff a)
def empty: Set[A] = Set.empty
}

Cats 的 Monoids 实现

1
2
import cats.Monoid
import cats.Semigroup

Object

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
import cats.Monoid
import cats.instances.string._ // for Monoid

Monoid[String].combine("Hi ", "there")
// res0: String = Hi there

Monoid[String].empty
// res1: String = ""

import cats.Semigroup

Semigroup[String].combine("Hi ", "there")
// res4: String = Hi there

import cats.Monoid
import cats.instances.int._ // for Monoid

Monoid[Int].combine(32, 10)
// res5: Int = 42

import cats.instances.option._ // for Monoid

val a = Option(22)
// a: Option[Int] = Some(22)

val b = Option(20)
// b: Option[Int] = Some(20)

Monoid[Option[Int]].combine(a, b)
// res6: Option[Int] = Some(42)

Syntax

1
2
3
4
5
6
7
8
9
10
import cats.instances.string._ // for Monoid
import cats.syntax.semigroup._ // for |+|

val stringResult = "Hi " |+| "there" |+| Monoid[String].empty
// stringResult: String = Hi there

import cats.instances.int._ // for Monoid

val intResult = 1 |+| 2 |+| Monoid[Int].empty
// intResult: Int = 3

使用案例

Monoid 是为某种类型提供的一种抽象的能力,这种能力是组合相加

SuperAdd

作为第一个案例,实现的功能很简单,就是实现一个 List 的累加,我们可以逐步提高这个累加功能的通用性:

累加 List[Int]

1
2
3
4
5
6
import cats.Monoid
import cats.instances.int._ // for Monoid
import cats.syntax.semigroup._ // for |+|

def add(items: List[Int]): Int =
items.foldLeft(Monoid[Int].empty)(_ |+| _)

这里利用了 foldLeft 算子来实现这个功能,如果不用 Monoid,可以直接使用该算子def add(items: List[Int]): Int = items.foldLeft(0)(_ + _)

累加 List[A]

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
import cats.Monoid
import cats.instances.int._ // for Monoid
import cats.syntax.semigroup._ // for |+|

def add[A](items: List[A])(implicit monoid: Monoid[A]): A =
items.foldLeft(monoid.empty)(_ |+| _)

def add[A: Monoid](items: List[A]): A =
items.foldLeft(Monoid[A].empty)(_ |+| _)

add(List(1, 2, 3))
// res9: Int = 6

add(List(Some(1), None, Some(2), None, Some(3)))
// res10: Option[Int] = Some(6)

如果 List 是一个泛型,这时候我们使用 Monoid 就有优势了,因为 Monoid 就是抽象了一种 combine 的能力,所以只要List 的泛型是 Monoid,就可以使用 Monoid 方法来处理合并。

累加自定义类

1
2
3
4
5
6
7
8
9
10
11
case class Order(totalCost: Double, quantity: Double)

implicit val monoid: Monoid[Order] = new Monoid[Order] {
def combine(o1: Order, o2: Order) =
Order(
o1.totalCost + o2.totalCost,
o1.quantity + o2.quantity
)

def empty = Order(0, 0)
}

上一节我们将 add 方法扩展到泛型,并且允许所有 Monoid 进行累加操作,对于基础类型,Cats 以及提供了Instance,对于自定义的类,需要自行实现 Monoid 的定义来处理 combine 操作。

Big Data

大数据合并结果:

  1. 计算访问人数,就是对 Int 的 Monoid 实例进行合并。
  2. 计算访问 unique 用户,就是对 Set[User] 的 Monoid 实例进行合并
  3. 计算 99% 相应时间,对 QTree 的 Monoid 实例进行合并

Distributed Systems

用于分布式系统一致性协议中,帮助进行不同节点数据的合并。

A particular class of data types support this reconciliation. These data types are called commutative replicated data types (CRDTs)

这里引出了一种数据类型:CRDTs,这就是一种具有 Monoid 实例的数据类型,具有合并的能力。我们在 Akka Distributed Data 的使用中会设计这种类型,帮助 Akka Cluster 实现数据的最终一致性。

总结

通过这个总结,我们用简短的语言再次描述一遍 MonoidSemiGroups

  1. 他们是 Type Classes 的一种
  2. 他们提供了合并或相加能力的抽象
  3. MonoidSemiGroups 都提供了合并或相加操作
  4. Monoid 额外多一个空集或零元
  5. 使用 Cats Monoid 符合 Type Classes 的三部曲:Type ClassesInstanceSyntax or Object
  6. Cats Monoid 三部曲 cats.Monoid cats.instances.string._ cats.syntax.semigroup._
  7. 最佳用途:为依赖累加的方法提供抽象

所以当你遇到需要对某些数据类型进行累加或者合并的时候,就需要考虑一下是否引入 Monoid