[译]Functor 与 Category (软件编写)(第六部分)

Smoke Art Cubes to Smoke — MattysFlicks — (CC BY 2.0) (译注:该图是用 PS 将烟雾处理成方块状后获得的效果,参见 flickr。))javascript

注意:这是 “软件编写” 系列文章的第六部分,该系列主要阐述如何在 JavaScript ES6+ 中从零开始学习函数式编程和组合化软件(compositional software)技术(译注:关于软件可组合性的概念,参见维基百科 Composability)。后续还有更多精彩内容,敬请期待!
<上一篇 | << 返回第一章前端

所谓 functor(函子),是可以对其进行 map 操做的对象。换言之,functor 能够被认为是一个容器,该容器容纳了一个值,而且暴露了一个接口(译注:即 map 接口),该接口使得外界的函数可以获取容器中的值。因此当你见到 functor,别被其来自范畴学的名字唬住,简单把他当作个 “mappable” 对象就行。java

“functor” 一词源于范畴学。在范畴学中,一个 functor 表明了两个范畴(category)间的映射。简单说来,一个 范畴 是一系列事物的分组,这里的 “事物” 能够指代一切的值。对于编码来讲,一个 functor 一般表明了一个具备 .map() 方法的对象,该方法可以将某一集合映射到另外一集合。react

上文说到,一个 functor 能够被看作是一个容器,好比咱们将其看作是一个盒子,盒子里面容纳了一些事物,或者空空如也,最重要的是,盒子暴露了一个 mapping(映射)接口。在 JavaScript 中,数组对象就是 functor 的绝佳例子(译注:[1,2,3].map(x => x + 1)),可是,其余类型的对象,只要可以被 map 操做,也能够算做是 functor,这些对象包括了单值对象(single valued-objects)、流(streams)、树(trees)、对象(objects)等等。android

对于如数组和流等其余这样的集合(collections)来讲,.map() 方法指的是,在集合上进行迭代操做,在此过程当中,应用一个预先指定的函数对每次迭代到的值进行处理。可是,不是全部的 functor 均可以被迭代。ios

在 JavaScript 中,数组和 Promise 对象都是 functor(Promise 对象虽然没有 .map() 方法,但其 .then() 方法也听从 functor 的定律),除此以外,很是多的第三方库也可以将各类各样的通常事物给转换成 functor(译注:大名鼎鼎的 Bluebird 就能将异步过程封装为 Promise functor)。git

在 Haskell 中,functor 类型被定义为以下形式:github

fmap :: (a -> b) -> f a -> f b复制代码

fmap 接受一个函数参数,该函数接受一个参数 a,并返回一个 b,最终,fmap 完成了从 f af b 的映射。f af b 能够被读做 “一个 a 的 functor” 和“一个 b 的 functor”,亦即 f a 这个容器容纳了 af b 这个容器容纳了 b编程

使用一个 functor 是很是简单的,仅须要调用 map() 方法便可:后端

const f = [1, 2, 3];
f.map(double); // [2, 4, 6]复制代码

Functor 定律

一个范畴含有两个基本的定律:

  1. 同一性(Identity)
  2. 组合性(Composition)

因为 functor 是两个范畴间的映射,其就必须遵照同一性和组合性,两者也构成了 functor 的基本定律。

同一性

若是你将函数(x => x)传入 f.map(),对任意的一个 functor ff.map(x => x) == f

const f = [1, 2, 3];
f.map(x => x); // [1, 2, 3]复制代码

组合性

functor 还必须具备组合性:F.map(x => f(g(x))) == F.map(g).map(f)

函数组合是将一个函数的输出做为另外一个函数输入的过程。例如,给定一个值 x及函数 f 和函数 g,函数的组合就是 (f ∘ g)(x)(一般简写为 f ∘ g,简写形式已经暗示了 (x)),其意味着 f(g(x))

不少函数式编程的术语都源于范畴学,而范畴学的实质便是组合。初看范畴学,就像初次进行高台跳水或者乘坐过山车,慌张,恐惧,可是并不难完成。你只需明确下面几个范畴学基础要点:

  • 一个范畴(category)是一个容纳了一系列对象及对象间箭头(->)的集合。
  • 箭头只是形式上的描述,实际上,箭头表明了态射(morphismms)。在编程中,态射能够被认为是函数。
  • 对于任何被箭头相链接的对象,如 a -> b -> c,必须存在一个 a -> c 的组合。
  • 全部的箭头表示都表明了组合(即使这个对象间的组合只是一个同一(identity)箭头:a->c)。全部的对象都存在一个同一箭头,即存在同一态射(a -> a)。

若是你有一个函数 g,该函数接受一个参数 a 而且返回一个 b,另外一个函数 f 接受一个 b 并返回一个 c。那么,必然存在一个函数 h,其表明了 fg 的组合。而 a -> c 的组合,就是 f ∘ g(读做f 紧接着 g),进而,也就是 h(x) = f(g(x))。函数组合的方向是由右向左的,这也就是就是 f ∘ g 常被叫作 f 紧接着 g 的缘由。

函数组合是知足结合律的,这就意味着你在组合多个函数时,免去了添加括号的烦恼:

h∘(g∘f) = (h∘g)∘f = h∘g∘f复制代码

让咱们再看一眼 JavaScript 中组合律:

给定一个 functor,F

const F = [1, 2, 3];复制代码

下面的两段是等效的:

F.map(x => f(g(x)));

// 等效于......

F.map(g).map(f);复制代码

译注:functor 中函数组合的结合率能够被理解为:对 functor 中保存的值使用组合后的函数进行 map,等效于前后对该值用不一样的函数进行 map。

Endofunctors(自函子)

一个 endofunctor(自函子)是一个能将一个范畴映射回相同范畴的 functor。

一个 functor 可以完成任意范畴间映射: F a -> F b

一个 endofunctor 可以完成相同范畴间的映射:F a -> F a

在这里,F 表明了一个 functor 类型,而 a 表明了一个范畴变量(意味着其可以表明任意的范畴,不管是一个集合,仍是一个包含了某一数据类型全部可能取值的范畴)。

而一个 monad 则是一个 endofunctor,先记住下面这句话:

“monad 是 endofunctor 范畴的 monoids(幺半群),有什么问题?”(译注:这句话的出处在该系列第一篇已有说起)

如今,咱们但愿第一篇说起的这句话能在以后多一点意义,monoids(幺半群)及 monad 将在以后做介绍。

自定义一个 Functor

下面将展现一个简单的 functor 例子:

const Identity = value => ({
  map: fn => Identity(fn(value))
});复制代码

显然,其知足了 functor 定律:

// trace() 是一个简单的工具函数来帮助审查内容
// 内容
const trace = x => {
  console.log(x);
  return x;
};

const u = Identity(2);

// 同一性
u.map(trace);             // 2
u.map(x => x).map(trace); // 2

const f = n => n + 1;
const g = n => n * 2;

// 组合性
const r1 = u.map(x => f(g(x)));
const r2 = u.map(g).map(f);

r1.map(trace); // 5
r2.map(trace); // 5复制代码

如今,你能够对存在该 functor 中的任何数据类型进行 map 操做,就像你对一个数组进行 map 时那样。这简直太美妙了。

上面的代码片展现了 JavaScript 中 functor 的简单实现,可是其缺失了 JavaScript 中常见数据类型的一些特性。如今咱们逐个添加它们。首先,咱们会想到,假如可以直接经过 + 操做符操做咱们的 functor 是否是很好,就像咱们在数值或者字符串对象间使用 + 号那样。

为了使该想法变现,咱们首先要为该 functor 对象添加 .valueOf() 方法 —— 这可被看做是提供了一个便捷的渠道来将值从 functor 盒子中取出。

const Identity = value => ({
  map: fn => Identity(fn(value)),

  valueOf: () => value,
});

const ints = (Identity(2) + Identity(4));
trace(ints); // 6

const hi = (Identity('h') + Identity('i'));
trace(hi); // "hi"复制代码

如今代码更漂亮了。可是若是咱们还想要在控制台审查 Identity 实例呢?若是控制台可以输出 "Identity(value)" 就太好了,为此,咱们只须要添加一个 .toString() 方法便可(译注:亦即重载原型链上原有的 .toString() 方法):

toString: () => `Identity(${value})`,复制代码

代码又有所进步。如今,咱们可能也想 functor 可以知足标准的 JavaScript 迭代协议(译注:MDN - 迭代协议)。为此,咱们能够为 Identity 添加一个自定义的迭代器:

[Symbol.iterator]: () => {
    let first = true;
    return ({
      next: () => {
        if (first) {
          first = false;
          return ({
            done: false,
            value
          });
        }
        return ({
          done: true
        });
      }
    });
  },复制代码

如今,咱们的 functor 还能这样工做:

// [Symbol.iterator] enables standard JS iterations:
const arr = [6, 7, ...Identity(8)];
trace(arr); // [6, 7, 8]复制代码

假如你想借助 Identity(n) 来返回包含了 n+1n+2 等等的 Identity 数组,这很是容易:

const fRange = (
  start,
  end
) => Array.from(
  {length: end - start + 1},
  (x, i) => Identity(i + start)
);复制代码

译注:MDN -- Array.from()

可是,若是你想上面的操做方式可以应用于任何 functor,该怎么办?假如咱们规定了每种数据类型对应的实例必须有一个关于其构造函数的引用,那么你能够这样改造以前的逻辑:

const fRange = (
  start,
  end
) => Array.from(
  {length: end - start + 1},

  // 将 `Identity` 变动为 `start.constructor`
  (x, i) => start.constructor(i + start)
);

const range = fRange(Identity(2), 4);
range.map(x => x.map(trace)); // 2, 3, 4复制代码

假如你还想知道一个值是否在一个 functor 中,又怎么办?咱们能够为 Identity 添加一个静态方法 .is() 来进行检测,另外,咱们也顺便添加了一个静态的 .toString() 方法来告知这个 functor 的种类:

Object.assign(Identity, {
  toString: () => 'Identity',
  is: x => typeof x.map === 'function'
});复制代码

如今,咱们整合一下上面的代码片:

const Identity = value => ({
  map: fn => Identity(fn(value)),

  valueOf: () => value,

  toString: () => `Identity(${value})`,

  [Symbol.iterator]: () => {
    let first = true;
    return ({
      next: () => {
        if (first) {
          first = false;
          return ({
            done: false,
            value
          });
        }
        return ({
          done: true
        });
      }
    });
  },

  constructor: Identity
});

Object.assign(Identity, {
  toString: () => 'Identity',
  is: x => typeof x.map === 'function'
});复制代码

注意,不管是 functor,仍是 endofunctor,不必定须要上述那么多的条条框框。以上工做只是为了咱们在使用 functor 时更加便捷,而非必须。一个 functor 的全部需求只是一个知足了 functor 定律 .map() 接口。

为何要使用 functor?

说 functor 多么多么好不是没有理由的。最重要的一点是,functor 做为一种抽象,能让开发者以同一种方式实现大量有用的,可以操纵任何数据类型的事物。例如,若是你想要在 functor 中值不为 null 或者不为 undefined 前提下,构建一串地链式操做:

// 建立一个 predicte
const exists = x => (x.valueOf() !== undefined && x.valueOf() !== null);

const ifExists = x => ({
  map: fn => exists(x) ? x.map(fn) : x
});

const add1 = n => n + 1;
const double = n => n * 2;

// undefined
ifExists(Identity(undefined)).map(trace);
// null
ifExists(Identity(null)).map(trace);

// 42
ifExists(Identity(20))
  .map(add1)
  .map(double)
  .map(trace)
;复制代码

函数式编程一直探讨的是将各个小的函数进行组合,以建立出更高层次的抽象。假如你想要一个更通用的,可以工做在任何 functor 上的 map() 方法,那么你能够经过参数的部分应用(译注:即 偏函数)来完成。

你可使用本身喜欢的 curry 化方法(译注:Underscore,Lodash,Ramda 等第三方库都提供了 curry 化一个函数的方法),或者使用下面这个以前篇章提到的,基于 ES6 的,充满魅力的 curry 化方法来实现参数的部分应用:

const curry = (
  f, arr = []
) => (...args) => (
  a => a.length === f.length ?
    f(...a) :
    curry(f, a)
)([...arr, ...args]);复制代码

如今,咱们能够自定义 map() 方法:

const map = curry((fn, F) => F.map(fn));

const double = n => n * 2;

const mdouble = map(double);
mdouble(Identity(4)).map(trace); // 8复制代码

总结

functor 是可以对其进行 map 操做的对象。更进一步地,一个 functor 可以将一个范畴映射到另外一个范畴。一个 functor 甚至能够将某一范畴映射回相同范畴(例如 endofunctor)。

一个范畴是一个容纳了对象和对象间箭头的集合。箭头表明了态射(也可理解为函数或者组合)。一个范畴中的每一个对象都具备一个同一态射(x -> x)。对于任何连接起来的对象 A -> B -> C,必存在一个 A -> C 的组合。

总之,functor 是一个极佳的高阶抽象,能然你建立各类各样的通用函数来操做任何的数据类型。

未完待续……

接下来

想学习更多 JavaScript 函数式编程吗?

跟着 Eric Elliott 学 Javacript,机不可失时再也不来!

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

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


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

相关文章
相关标签/搜索