The Monoid of FP
什么是 Monoid
在《The Semigroup of FP》中,我们介绍了 Semigroup 以及它的超集 Magma,这篇文章我们来介绍 Monoid。
首先,我们先直观地表示 Monoid 与 Semigroup 还有 Magma 的关系,如下图:
在代码上,可以这样声明:
import { Semigroup } from "fp-ts/Semigroup";
interface Monoid<A> extends Semigroup<A> {
readonly empty: A;
}
可以发现,Monoid 是 Semigroup 的子集,因此,关于 Monoid 的定义,即是满足某种条件的 Semigroup,这个条件是存在某个值(通常称作**empty**
**),**满足如下约束:
- Left Identity:
concat(x, empty) = x
- Right Identity:
concat(empty, x) = x
empty
具有唯一性
这两个约束的语义是,无论我们把empty
这个值concat
到x
的哪边,它们的结果是等价的,比如:
0
对于SemigroupSum<number>
来说,concat(1, 0) = 1
且concat(0, 1) = 1
1
对于SemigroupProduct<number>
来说,concat(2, 1) = 2
且concat(1, 2) = 2
''
对于SemigroupAppend<string>
来说,concat('abc', '') = 'abc'
且concat('', 'abc') = 'abc'
但如果SemigroupAppend<string>
的定义是:
const semigroupAppend: Semigroup<string> = {
concat: (x, y) => x + "+" + y,
};
// it is not a monoid
semigroupAppend.concat("abc", ""); // abc+
semigroupAppend.concat("", "abc"); // +abc
则它不满足 Monoid 的约束,因为不存在一个唯一的empty
的值,使concat(x, empty) = x
和concat(empty, x) = x
等价。
Folding
在 Semigroup 中,针对 Folding 的场景,我们需要提供一个初始值来保证当对象集合为空时,最终返回值始终是一致的。
在 Monoid 中,我们则不需要这个初始值,这是因为empty
这个值可以用来表示这种情况下的状态,因此,使用 Monoid 来做 Folding 逻辑在代码实现上,会更简洁,如下(引用fp-ts
的例子):
import { fold } from "fp-ts/Monoid";
fold(monoidSum)([1, 2, 3, 4]); // 10
fold(monoidProduct)([1, 2, 3, 4]); // 24
fold(monoidString)(["a", "b", "c"]); // 'abc'
fold(monoidAll)([true, false, true]); // false
fold(monoidAny)([true, false, true]); // true
Monoid 的应用
使用 canvas 绘制图形
https://codesandbox.io/s/distracted-jang-ypsj89?file=/src/index.ts
第一个例子引用functional-programming
中,讲解 Monoid 中的例子来理解它的应用。
例子中代码实现的逻辑是在canvas
中绘制图形,虽然简单绘制图形的命令式实现也比较简单,但例子中使用 Monoid 来抽象,绘制多个可能发生重叠的图形(Intersection Shapes)以及多个需要进行组合的图形(Union Shapes)。
回填可选字段的默认值
第二个例子源自fp-ts
官方文档,它使用getLastMonoid
获取多个 Option 中,最靠右(或最靠后)的非空值,如下:
x | y | concat(x, y) |
---|---|---|
none | none | none |
some(a) | none | some(a) |
none | some(a) | some(a) |
some(a) | some(b) | some(b) |
这个逻辑在业务开发中经常会用到,比如:
- 查询接口中的如果每个参数未设置,要使用它的默认参数,如分页
- 全局设置与局部设置,局部设置如果存在,则局部设置生效,反之则全局设置生效(例子中就实现了该逻辑)
- 异步获取数据时,处于异步状态下的数据状态应当处于一个提前约定的默认值
import { Monoid, getStructMonoid } from "fp-ts/Monoid";
import { Option, some, none, getLastMonoid } from "fp-ts/Option";
/** VSCode settings */
interface Settings {
/** Controls the font family */
fontFamily: Option<string>;
/** Controls the font size in pixels */
fontSize: Option<number>;
/** Limit the width of the minimap to render at most a certain number of columns. */
maxColumn: Option<number>;
}
const monoidSettings: Monoid<Settings> = getStructMonoid({
fontFamily: getLastMonoid<string>(),
fontSize: getLastMonoid<number>(),
maxColumn: getLastMonoid<number>(),
});
const workspaceSettings: Settings = {
fontFamily: some("Courier"),
fontSize: none,
maxColumn: some(80),
};
const userSettings: Settings = {
fontFamily: some("Fira Code"),
fontSize: some(12),
maxColumn: none,
};
/** userSettings overrides workspaceSettings */
monoidSettings.concat(workspaceSettings, userSettings);
/*
{ fontFamily: some("Fira Code"),
fontSize: some(12),
maxColumn: some(80) }
*/