The Monoid of FP

什么是 Monoid

在《The Semigroup of FP》中,我们介绍了 Semigroup 以及它的超集 Magma,这篇文章我们来介绍 Monoid。
首先,我们先直观地表示 Monoid 与 Semigroup 还有 Magma 的关系,如下图:
magma-vs-semigroup-vs-monoid
在代码上,可以这样声明:

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这个值concatx的哪边,它们的结果是等价的,比如:

  • 0对于SemigroupSum<number>来说,concat(1, 0) = 1concat(0, 1) = 1
  • 1对于SemigroupProduct<number>来说,concat(2, 1) = 2concat(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) = xconcat(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 中,最靠右(或最靠后)的非空值,如下:

xyconcat(x, y)
nonenonenone
some(a)nonesome(a)
nonesome(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) }
*/