[译]JavaScript 让 Monad 更简单(软件编写)(第十一部分)

JavaScript 让 Monad 更简单(软件编写)(第十一部分)

Smoke Art Cubes to Smoke — MattysFlicks — (CC BY 2.0)
Smoke Art Cubes to Smoke — MattysFlicks — (CC BY 2.0)

(译注:该图是用 PS 将烟雾处理成方块状后获得的效果,参见 flickr。)javascript

这是 “软件编写” 系列文章的第十一部分,该系列主要阐述如何在 JavaScript ES6+ 中从零开始学习函数式编程和组合化软件(compositional software)技术(译注:关于软件可组合性的概念,参见维基百科
< 上一篇 | << 返回第一篇前端

在开始学习 Monad 以前,你应当了解过:java

  • 函数组合:compose(f, g)(x) = (f ∘ g)(x) = f(g(x))
  • Functor 基础:对于 Array.map() 操做有清晰的理解

Gilad Bracha 曾说过,“一旦你明白了 monad,你反而就无法向其余人解释什么是 monad 了”,这就好像 Lady Mondegreen 空耳诅咒同样,咱们均可以称其为 Lady Monadgreen 诅咒了。(Gilad Bracha 这段话最著名的引用者你不会陌生,他是 Douglas Crockford)。react

译注:Mondegreen 指空耳,Lady Modegreen 是该词的来源,当年一个小女孩把 “and laid him on the green” 错听成了 “and Lady Mondegreen”。android

Kurt Vonnegut's 在其小说 Cat's Cradle 中写到:“Hoenikker 博士常说,任何没法对一个 8 岁大的孩子解释清楚他是作什么的科学家都是骗子”。ios

若是你在网上搜索 “Monad”,你会被各类范畴学理论搞得头皮发麻,不少人也貌似 “颇有帮助地” 用各类术语去解释它。git

可是,别被那些专业术语给唬住了,Monad 其实很简单。咱们看一下 Monad 的本质。github

一个 Monad 是一种组合函数的方式,它除了返回值之外,还须要一个 context。常见的 Monad 有计算任务,分支任务,或者 I/O 操做。Monad 的 type lift(类型提高),flatten(展平)以及 map(映射)操做使得数据类型统一,从而实现了,即使组合链中存在 a => M(b) 这样的类型提高,函数仍然可组合。a => M(b) 是一个伴随着某个计算 context 的映射过程,Monad 经过 type lift,flatten 及 map 完成,可是用户不须要关心实现细节:编程

  • 函数 map: a => b
  • 具备 Functor context 的 map: Functor(a) => Functor(b)
  • 具有 Monad context,且须要 flatten 的 map:Monad(Monad(a)) => Monad(b)

可是,“flatten”、“map” 和 “context” 究竟意味着什么?后端

  • map 指的是,“应用一个函数到 a,返回 b”。即给定某输入,返回某输出。
  • context 是一个 Monad 组合(包括 type lift,flatten 和 map)的计算细节。Functor/Monad 的 API 用到了 context,这些 API 容许你在应用的某些部分组合 Monad。Functor 及 Monad 的核心在于将 context 进行抽象,使咱们在进行组合的时候不须要关注其中细节。在 context 内部进行 map 意味着你能够在 context 内部应用一个 map 函数完成 a => b,而新返回的 b 又被包裹了相同的 context。若是 a 的 context 是 Observable,那么 b 的 context 就也是 Observable,即 Observable(a) => Observable(b)。同理有,Array(a) => Array(b)
  • type lift 指的是将一个类型提高到对应的 context 中,值所以被赋予了对应 context 拥有的 API 用于计算,驱动 context 相关计算等等。类型提高能够描述为 a => F(a)。(Monad 也是一种 Functor,因此这里咱们用了 F 表示 Monad)
  • flatten 指的是去除值的 context 包裹。即 F(a) => a

上面的说明仍是有些抽象,如今看个例子:

const x = 20;             // `a` 数据类型的 `x`
const f = n => n * 2;     // 将 `a` 映射为 `b` 的函数
const arr = Array.of(x);  // 提高 `x` 的类型为 Array
// JavaScript 中对于数组类型的提高可使用语法糖:`[x]`
// `Array.prototype.map()` 在 `x` 上应用了 map 函数 `f`,
// map 发生的 context 正是数组
const result = arr.map(f); // [40]复制代码

在这个例子中,Array 就是 context,x 是进行 map 的值。

这个例子没有涉及嵌套数组,可是在 JavaScript 中,你能够经过 .concat() 展开数组:

[].concat.apply([], [[1], [2, 3], [4]]); // [1, 2, 3, 4]复制代码

你早就用过 Monad 了

不管你对范畴学知道多少,使用 Monad 都会优化你的代码。不知道利用 Monad 的好处的代码就可能让人头疼,如回调地狱,嵌套的条件分支,冗余代码等。

本系列已经不厌其烦的说过,软件开发的本质便是组合,而 Monad 使得组合更加容易。再回顾下 Monad 的实质:

  • 函数 map,这要求函数的输入输出是整齐划一的: a => b
  • 具备 Functor context 的 map,要求函数的输入输出是 Functor: Functor(a) => Functor(b)
  • 具有 Monad context,且须要 flatten 的 map,则容许组合中发生类型提高:Monad(Monad(a)) => Monad(b)

这些都是描述函数组合的不一样方式。函数存在的真正目的就是让你去组合他们,编写应用。函数帮助你将复杂问题划分为若干简单问题,从而可以分而治之的处理这些小问题,在应用中,不一样的函数组合,就带来了解决不一样问题的方式,从而让你不管面对什么大的问题,都能经过组合进行解决。

理解函数及如何正确使用函数的关键在于更深入地认识函数组合。

函数组合是为数据流建立一个包含有若干函数的管道。在管道入口,你导入数据,在管道出口,你得到了加工好的数据。但为了让管道工做,管道上的每一个函数接受的输入应当与上一步函数的输出拥有一样的数据类型。

组合简单函数很是容易,由于函数的输入输出都有整齐划一的类型。只须要匹配输出类型 b 为 输入类型 b 便可:

g:           a => b
f:                b => c
h = f(g(a)): a    =>   c复制代码

若是你的映射是 F(a) => F(b),使用 Functor 的组合也很容易完成,由于这个组合中的数据类型也是整齐划一的:

g:             F(a) => F(b)
f:                     F(b) => F(c)
h = f(g(Fa)):  F(a)    =>      F(c)复制代码

可是若是你想要从 a => F(b)b => F(c) 这样的形式进行函数组合,你就须要 Monad。咱们把 F() 换为 M() 从而让你知道 Monad 该出场了:

g:                  a => M(b)
f:                       b => M(c)
h = composeM(f, g): a    =>   M(c)复制代码

等等,在这个例子中,管道中流通在函数之间的数据类型没有整齐划一。函数 f 接收的输入是类型 b,可是上一步中,fg 处拿到的类型倒是 M(b)(装有 b 的 Monad)。因为这一不对称性,composeM() 须要展开 g 输出的 M(b),把得到的 b 传给 f,由于 f 想要的类型是 b 而不是 M(b)。这一过程(一般称为 .bind() 或者 .chain()) 就是 flatten 和 map 发生的地方。

下面的例子中展示了 flatten 的过程:从 M(b) 中取出 b 并传递给下一个函数:

g:             a => M(b) flattens to => b
f:                                      b           maps to => M(c)
h composeM(f, g):
               a       flatten(M(b)) => b => map(b => M(c)) => M(c)复制代码

Monad 使得类型整齐划一,从而使 a => M(b) 这样,发生了类型提高的函数也可被组合。

在上面的图示中,M(b) => b 的 flatten 操做及 b => M(c) 的 map 操做都在 chain 方法内部完成了。chain 的调用发生在了 composeM() 内部。在应用层面,你不须要关注内在的实现,你只须要用和组合通常函数相同的手段组合返回 Monad 的函数便可。

因为大多数函数都不是简单的 a => b 映射,所以 Monad 是须要的。一些函数须要处理反作用(如 Promise,Stream),一些函数须要操纵分支(Maybe),一些函数须要处理异常(Either),等等。

这儿有一个更加具体的例子。假如你须要从某个异步的 API 中取得某用户,以后又将该用户传给另外一个异步 API 以执行某个计算:

getUserById(id: String) => Promise(User)
hasPermision(User) => Promise(Boolean)复制代码

让咱们撰写一些函数来验证 Monad 的必要性。首先,建立两个工具函数,compose()trace()

const compose = (...fns) => x => fns.reduceRight((y, f) => f(y), x);
const trace = label => value => {
  console.log(`${ label }: ${ value }`);
  return value;
};复制代码

以后,尝试进行函数组合解决问题(根据 Id 得到用户,进而判断用户是否具备某个权限):

{
  const label = 'API call composition';
  // a => Promise(b)
  const getUserById = id => id === 3 ?
    Promise.resolve({ name: 'Kurt', role: 'Author' }) :
    undefined
  ;
  // b => Promise(c)
  const hasPermission = ({ role }) => (
    Promise.resolve(role === 'Author')
  );
  // 尝试组合上面两个任务,注意:这个例子会失败
  const authUser = compose(hasPermission, getUserById);
  // 老是输出 false
  authUser(3).then(trace(label));
}复制代码

当咱们尝试组合 hasPermission()getUserById()authUser() 时,咱们遇到了一个大问题,因为 hasPermission() 接收一个 User 对象做为输入,但却获得的是 Promise(User)。为了解决这个问题,咱们须要建立一个特别地组合函数 composePromises() 来替换掉原来的 compose(),这个组合函数知道使用 .then() 去完成函数组合:

{
  const composeM = chainMethod => (...ms) => (
    ms.reduce((f, g) => x => g(x)[chainMethod](f))
  );
  const composePromises = composeM('then');
  const label = 'API call composition';
  // a => Promise(b)
  const getUserById = id => id === 3 ?
    Promise.resolve({ name: 'Kurt', role: 'Author' }) :
    undefined
  ;
  // b => Promise(c)
  const hasPermission = ({ role }) => (
    Promise.resolve(role === 'Author')
  );
  // 组合函数,此次大功告成了!
  const authUser = composePromises(hasPermission, getUserById);
  authUser(3).then(trace(label)); // true
}复制代码

稍后咱们会讨论 composeM() 的细节。

再次牢记 Monad 的实质:

  • 函数 map: a => b
  • 具备 Functor context 的 map: Functor(a) => Functor(b)
  • 具有 Monad context,且须要 flatten 的 map:Monad(Monad(a)) => Monad(b)

在这个例子中,咱们的 Monad 是 Promise,因此当咱们组合这些返回 Promise 的函数时,对于 hasPermission() 函数,它获得的是 Promise(User) 而不是 Promise 中装有的 User 。注意到,若是你去除了 Monad(Monad(a)) 中外层 Monad() 的包裹,就剩下了 Monad(a) => Monad(b),这就是 Functor 中的 .map()。若是咱们再有某种手段可以展开 Monad(x) => x 的话,就走上正轨了。

Monad 的构成

每一个 Monad 都是基于一种简单的对称性 -- 一个将值包裹到 context 的方式,以及一个取消 context 包裹,将值取出的方式:

  • Lift/Unit:将某个类型提高到 Monad 的 context 中:a => M(a)
  • Flatten/Join:去除 context 包裹:M(a) => a

因为 Monad 也是 Functor,所以它们可以进行 map 操做:

  • Map:进行保留 context 的 map:M(a) -> M(b)

组合 flatten 以及 map,你就能获得 chain -- 这是一个用于 monad-lifting 函数的函数组合,也称之为 Kleisli 组合,名称来自 Heinrich Kleisli

  • FlatMap/Chain: flatten 之后再进行 map:M(M(a)) => M(b)

对于 Monad 来讲,.map() 方法一般从公共 API 中省略了。type lift 和 flatten 不会显示地要求 .map() 调用,但你已经有了 .map() 所须要的所有。若是你可以 lift(也称为 of/unit) 以及 chain(也称为 bind/flatMap),你就能完成 .map(),即完成 Monad 中值的映射:

const MyMonad = value => ({
  // <... 这里能够插入任意的 chain 和 of ...>
  map (f) {
    return this.chain(a => this.constructor.of(f(a)));
  }
});复制代码

因此,若是你为 Monad 定义了 .of().chain() 或者 .join() ,你就能够推导出 .map() 的定义。

lift 能够由工厂函数、构造方法或者 constructor.of() 完成。在范畴学中,lift 叫作 “unit”。list 完成的是将某个类型提高到 Monad context。它将某个 a 转换到了一个包裹着 a 的 Monad。

在 Haskell 中,很使人困惑的是,lift 被叫作 return,通常咱们认为的 return 指的都是函数返回。我仍有意将它称之为 “lift” 或者 “type lift”,并在代码中使用 .of() 完成 lift,这样更符合咱们的理解。

flatten 过程一般被叫作 flatten() 或者 join()。多数时候,咱们用不上 flatten() 或者 join(),由于它们内联到了 .chain() 或者 .flatMap() 中。flatten 一般会配合上 map 操做在组合中使用,由于去除 context 包裹以及 map 都是组合中 a => M(a) 须要的。

去除某类 Monad 多是很是简单的。例如 Identity Monad,Identity Monad 的 flatten 过程相似它的 .map() 方法,只不过你不用将返回的值提高回 Monad context。Identity Monad 去除一层包裹的例子以下:

{ // Identity monad
const Id = value => ({
  // Functor Maping
  // 经过将被 map 的值传入到 type lift 方法 .of() 中
  // 使得 .map() 维持住了 Monand context 包裹:
  map: f => Id.of(f(value)),
  // Monad chaining
  // 经过省略 .of() 进行的类型提高
  // 去除了 context 包裹,并完成 map
  chain: f => f(value),
  // 一个简便方法来审查 context 包裹的值:
  toString: () => `Id(${ value })`
});
// 对于 Identity Monad 来讲,type lift 函数只是这个 Monad 工厂的引用
Id.of = Id;复制代码

可是去除 context 包裹也会与诸如反作用,错误分支,异步 IO 这些怪家伙打交道。在软件开发过程当中,组合是真正有意思的事儿发生的地方。

例如,对于 Promise 对象来讲,.chain() 被称为 .then()。调用 promise.then(f) 不会当即 f()。取而代之的是,then(f) 会等到 Promise 对象被 resolve 后,才调用 f() 进行 map,这也是 then 命名的来由:

{
  const x = 20;                 // 值
  const p = Promise.resolve(x); // context
  const f = n => 
    Promise.resolve(n * 2);     // 函数
  const result = p.then(f);     // 应用程序
  result.then(
    r => console.log(r)         // 结果:40
  );
}复制代码

对于 Promise 对象,.then() 就用来替代 .chain(),但其实两者完成的是同一件事儿。

可能你听到说 Promise 不是严格意义上的 Monad,这是由于只有 Promise 包裹的值是 Promise 对象时,.then() 才会去除外层 Promise 的包裹,不然它会直接作 .map(),而不须要 flatten。

可是因为 .then() 对 Promise 类型的值和其余类型的值处理不一样,所以,它不会严格遵照数学上 Functor 和 Monad 对任何值都必须遵照的定律。实际上,只要你知道 .then() 在处理不一样数据类型上的差别,你也能够把它当作是 Monad。只须要留意一些通用组合工具可能没法工做在 Promise 对象上。

构建 monadic 组合(也叫作 Kleisli 组合)

让咱们深刻到 composeM 函数里面看看,这个函数咱们用来组合 promise-lifting 的函数:

const composeM = method => (...ms) => (
  ms.reduce((f, g) => x => g(x)[method](f))
);复制代码

藏在古怪 reducer 里面的是函数组合的代数定义:f(g(x))。若是咱们想要更好地理解 composeM,先看看下面的代码:

{
  // 函数组合的算数定义:
  // (f ∘ g)(x) = f(g(x))
  const compose = (f, g) => x => f(g(x));
  const x = 20;    // 值
  const arr = [x]; // 值的容器
  // 待组合的函数
  const g = n => n + 1;
  const f = n => n * 2;
  // 下面代码证实了 .map() 完成了函数组合
  // 对 map 的链式调用完成了函数组合
  trace('map composes')([
    arr.map(g).map(f),
    arr.map(compose(f, g))
  ]);
  // => [42], [42]
}复制代码

这段代码意味着咱们能够撰写一个泛化的组合工具来服务于任何可以应用 .map() 方法的 Fucntor,例如数组等:

const composeMap = (...ms) => (
  ms.reduce((f, g) => x => g(x).map(f))
);复制代码

这个函数是 f(g(x)) 另外一个表述形式。给定任意数量的、发生类型提高的函数 a -> Functor(b),迭代待组合的函数,它们接受输入 x,并经过 .map(f) 完成 map 和 type lift。.reduce() 方法接受一个两参数函数:一个参数是累加器(本例中是 f,表示组合后的函数),另外一个参数是当前值(本例中是当前函数 g)。

每次迭代都返回了一个新的函数 x => g(x).map(f),这个新函数也是下一次迭代中的 f。咱们已经证实 x => g(x).map(f) 等同于将 compose(f, g)(x) 的值提高到 Functor 的 context 中。换言之,即等同于对 Functor 中的值应用 f(g(x)),在本例中,这指的是对原数组中的值应用组合后的函数进行 map。

性能警告:我不建议对数组这么作。以这种方式组合函数将要求对整个数组进行多重迭代,假如数组规模很大,这样作的时间开销很大。对于数组进行 map,要么进行简单函数组合 a -> b,再在数组上一次性应用组合后的函数,要么优化 .reducer() 的迭代过程,要么直接使用一个 transducer。

译注:transducer 是一个函数,其名称复合了 transform 和 reducer。transducer 即为每次迭代指明了 tramsform 的 reducer:

const increment = x => x + 1
const square = x => x * x
const transducer = R.map(R.compose(square, increment))
const data = [1, 2, 3]
const initialData = [0]
const accumulator = R.flip(R.append)
R.transduce(transducer, accumulator, initialData, data) // => [0, 4, 9, 16]复制代码

上述代码至关于:

const increment = x => x + 1
const square = x => x * x
const transform = R.compose(square, increment)
const data = [1, 2, 3]
const initialData = [0]
data.reduce((acc, curr) => acc.concat([transform(curr)]), initialData) // => [0, 4, 9, 16]复制代码

参考资料: ramda .transduce()

对于同步任务,数组的映射函数都是当即执行的,所以须要关注性能。然而,多数的异步任务都是延迟执行的,而且这部分任务一般须要应对异常或者空值这样的使人头痛分支情况。

这样的场景对 Monad 再合适不过了。在组合链中,当前 Monad 须要的值须要上一步异步任务或者分支完成时才能得到。在这些情景下,你没法在组合外部拿到值,由于它们被一个 context 包裹住了,组合过程是 a => Monad(b) 而不是 a => b

不管什么时候你的一个函数接收了一些数据,触发了一个 API,返回了对应的值,另外一个函数接收了这些值,触发了另外一个 API,而且返回了这些数据的计算结果,你会想要使用 a => Monad(b) 来组合这些函数。因为 API 调用是异步的,你会须要将返回值包上相似 Promise 或者 Observable 这样的 context。换句话说,这些函数的签名会是 a -> Monad(b) 以及 b -> Monad(c)

组合 g: a -> b, f: b -> c 类型的函数是很简单的,由于输入输出是整齐划一的。h: a -> c 这个变化只须要 a => f(g(a))

组合 g: a -> Monad(b), f: b -> Monad(c) 就稍微有些困难。h: a -> Monad(c) 这个变化不能经过 a => f(g(a)) 完成,由于 f() 须要的是 b,而不是 Monad(b)

让咱们看一个更具体的例子,咱们组合了一系列异步任务,它们都返回 Promise 对象:

{
  const label = 'Promise composition';
  const g = n => Promise.resolve(n + 1);
  const f = n => Promise.resolve(n * 2);
  const h = composePromises(f, g);
  h(20)
    .then(trace(label))
  ;
  // Promise composition: 42
}复制代码

怎么才能写一个 composePromises() 对异步任务进行组合,并得到预期输出呢?提示:你以前可能见到过。

对的,就是咱们提到过的 composeMap() 函数?如今,你只须要将其内部使用的 .map() 换成 .then() 便可,Promise.then() 至关于异步的 .map()

{
  const composePromises = (...ms) => (
    ms.reduce((f, g) => x => g(x).then(f))
  );
  const label = 'Promise composition';
  const g = n => Promise.resolve(n + 1);
  const f = n => Promise.resolve(n * 2);
  const h = composePromises(f, g);
  h(20)
    .then(trace(label))
  ;
  // Promise composition: 42
}复制代码

稍微有些古怪的地方在于,当你触发了第二个函数 f,传给 f 的不是它想要的 b,而是 Promise(b),所以 f 须要去除 Promise 包裹,拿到 b。接下来该怎么作呢?

幸运的是,在 .then() 内部,已经拥有了一个将 Promise(b) 展平为 b 的过程了,这个过程一般称之为 join 或者 flatten

也许你已经留意到了 composeMap()composePromise() 的实现几乎同样。所以咱们建立一个高阶函数来为不一样的 Monad 建立组合函数。咱们只须要将链式调用须要的函数混入一个柯里化函数便可,以后,使用方括号包裹这个链式调用须要的方法名:

const composeM = method => (...ms) => (
  ms.reduce((f, g) => x => g(x)[method](f))
);复制代码

如今,咱们能针对性地为不一样的 Monad 建立组合函数:

const composePromises = composeM('then');
const composeMap = composeM('map');
const composeFlatMap = composeM('flatMap');复制代码

Monda 定律

在你开始建立你的 Monad 以前,你须要知道全部的 Monad 都要知足的一些定律:

  1. 左同一概: unit(x).chain(f) ==== f(x)(译注:将 x 提高到 Monad context 后,使用 f() 进行 map,等同于直接对 x 直接使用 f 进行 map)
  2. 右同一概: m.chain(unit) ==== m(译注:Monad 对象进行 map 操做的结果等于原对象 )
  3. 结合律: m.chain(f).chain(g) ==== m.chain(x => f(x).chain(g))

同一概(Identity Law)

左同一概及右同一概
左同一概及右同一概

一个 Monad 也是一个 Functor。一个 Functor 是两个范畴之间一个态射(morphism):A -> B,其中箭头符号即描述了态射。除了对象间显式的态射,每个范畴中的对象也拥有一个指向本身的箭头。换言之,对于范畴中的每个对象 X,存在着一个箭头 X -> X。该箭头称之为同一(identity)箭头,一般使用一个从自身出发并指回自身的弧形箭头表示。

同一态射
同一态射

结合律(Associativity)

结合律意味着咱们不须要关心咱们组合时在哪里放置括号。若是咱们是在作加法,加法有结合律: a + (b + c) 等同于 (a + b) + c。这对于函数组合也一样适用: (f ∘ g) ∘ h = f ∘ (g ∘ h)

而且,这对于 Kleisli 组合仍然适用。对于这种组合,你应该从前日后地看,把组合运算 chain 看成是 after 便可:

h(x).chain(x => g(x).chain(f)) ==== (h(x).chain(g)).chain(f)复制代码

Monda 的定律证实

接下来咱们证实同一 Monad 知足 Monad 定律:

{ // Identity monad
  const Id = value => ({
    // Functor Maping
    // 经过将被 map 的值传入到 type lift 方法 .of() 中
    // 使得 .map() 维持住了 Monand context 包裹:
    map: f => Id.of(f(value)),
    // Monad chaining
    // 经过省略 .of() 进行的类型提高
    // 去除了 context 包裹,并完成 map
    chain: f => f(value),
    // 一个简便方法来审查 context 包裹的值:
    toString: () => `Id(${ value })`
  });

  // 对于 Identity Monad 来讲,type lift 函数只是这个 Monad 工厂的引用
  Id.of = Id;
  const g = n => Id(n + 1);
  const f = n => Id(n * 2);
  // 左同一概
  // unit(x).chain(f) ==== f(x)
  trace('Id monad left identity')([
    Id(x).chain(f),
    f(x)
  ]);
  // Id Monad 左同一概: Id(40), Id(40)

  // 右同一概
  // m.chain(unit) ==== m
  trace('Id monad right identity')([
    Id(x).chain(Id.of),
    Id(x)
  ]);
  // Id Monad right identity: Id(20), Id(20)

  // 结合律
  // m.chain(f).chain(g) ====
  // m.chain(x => f(x).chain(g)  
  trace('Id monad associativity')([
    Id(x).chain(g).chain(f),
    Id(x).chain(x => g(x).chain(f))
  ]);
  // Id monad associativity: Id(42), Id(42)
}复制代码

总结

Monad 是组合类型提高函数的方式:g: a => M(b), f: b => M(c)。为了作到,Monad 必须在应用函数 f() 以前,展平 M(b) 取出 b 交给 f()。换言之,Functor 是你能够进行 map 操做的对象,而 Monad 是你能够进行 flatMap 操做的对象:

  • 函数 map: a => b
  • 具备 Functor context 的 map: Functor(a) => Functor(b)
  • 具有 Monad context,且须要 flatten 的 map:Monad(Monad(a)) => Monad(b)

每一个 Monad 都是基于一种简单的对称性 -- 一个将值包裹到 context 的方式,以及一个取消 context 包裹,将值取出的方式:

  • Lift/Unit:将某个类型提高到 Monad 的 context 中:a => M(a)
  • Flatten/Join:去除 context 包裹:M(a) => a

因为 Monad 也是 Functor,所以它们可以进行 map 操做:

  • Map:进行保留 context 的 map:M(a) -> M(b)

组合 flatten 以及 map,你就能获得 chain -- 这是一个用于 monad-lifting 函数的函数组合,也称之为 Kleisli 组合。

  • FlatMap/Chain: flatten 之后再进行 map:M(M(a)) => M(b)

Monads 必须知足三个定律(公理),合在一块儿称之为 Monad 定律:

  • 左同一概:unit(x).chain(f) ==== f(x)
  • 右同一概:m.chain(unit) ==== m
  • 结合律:m.chain(f).chain(g) ==== m.chain(x => f(x).chain(g)

天天撰写 JavaScript 代码的时候,你或多或少已经在使用 Monad 或者 Monad 相似的东西了,例如 Promise 和 Observable。Kleisli 组合容许你组合数据流逻辑时不用操心组合中的数据类型,也不用担忧可能发生的反作用,条件分支,以及其余一些组合中去除 context 包裹时的细节,这些细节所有都藏在了 .chain() 操做中。

这一切都让 Monad 在简化代码中扮演了重要角色。在阅读文本以前,兴许你还不明白 Monad 内部到底作了什么就已经从 Monad 中受益颇丰,如今,你对 Monad 底层细节也有了必定认识,这些细节也并不可怕。

回到开头,咱们不用再害怕 Lady Monadgreen 的诅咒了。

经过一对一辅导提高你的 JavaScript 技巧

DevAnyWhere 能帮助你最快进阶你的 JavaScript 能力:

  • 直播课程
  • 灵活的课时
  • 一对一辅导
  • 构建真正的应用产品

https://devanywhere.io/
https://devanywhere.io/

Eric Elliott“编写 JavaScript 应用” (O’Reilly) 以及 “跟着 Eric Elliott 学 Javascript” 两书的做者。他为许多公司和组织做过贡献,例如 Adobe SystemsZumba FitnessThe Wall Street JournalESPNBBC 等 , 也是不少机构的顶级艺术家,包括但不限于 UsherFrank Ocean 以及 Metallica

大多数时间,他都在 San Francisco Bay Area,同这世上最美丽的女子在一块儿。

掘金翻译计划 是一个翻译优质互联网技术文章的社区,文章来源为 掘金 上的英文分享文章。内容覆盖 AndroidiOSReact前端后端产品设计 等领域,想要查看更多优质译文请持续关注 掘金翻译计划官方微博知乎专栏

相关文章
相关标签/搜索