[譯]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前端後端產品設計 等領域,想要查看更多優質譯文請持續關注 掘金翻譯計劃

相關文章
相關標籤/搜索