[譯] Lenses:可組合函數式編程的 Getter 和 Setter(第十九部分)

注意:本篇是「組合軟件」這本書 的一部分,它將以系列博客的形式展開新生。它涵蓋了 JavaScript(ES6+)函數式編程和可組合軟件技術的最基礎的知識。 < 上一篇 | << 從第一部分開始javascript

lens 是一對可組合的 getter 和 setter 純函數,它會關注對象內部的一個特殊字段,而且會聽從一系列名爲 lens 法則的公理。將對象視爲總體,字段視爲局部。getter 以對象總體做爲參數,而後返回 lens 所關注的對象的一部分。前端

// view = whole => part
複製代碼

setter 則以對象總體做爲參數,以及一個須要設置的值,而後返回一個新的對象總體,這個對象的特定部分已經更新。和一個簡單設置對象成員字段的值的函數不一樣,Lens 的 setter 是純函數:java

// set = whole => part => whole
複製代碼

注意:在本篇中,咱們將在代碼示例中使用一些原生的 lenses,這樣是爲了對整體概念有更深刻的瞭解。而對於生產環境下的代碼,你則應該看看像 Ramda 這樣的通過充分測試的庫。不一樣的 lens 庫的 API 也不一樣,比起本篇給出的例子,更有可能用可組合性更強、更優雅的方法來描述 lenses。android

假設你有一個元組數組(tuple array),表明了一個包含 xyz 三點的座標:ios

[x, y, z]
複製代碼

爲了能分別獲取或者設置每一個字段,你能夠建立三個 lenses。每一個軸一個。你能夠手動建立關注每一個字段的 getter:git

const getX = ([x]) => x;
const getY = ([x, y]) => y;
const getZ = ([x, y, z]) => z;

console.log(
  getZ([10, 10, 100]) // 100
);
複製代碼

一樣,相應的 setter 也許會像這樣:github

const setY = ([x, _, z]) => y => ([x, y, z]);

console.log(
  setY([10, 10, 10])(999) // [10, 999, 10]
);
複製代碼

爲何選擇 Lenses?

狀態依賴是軟件中耦合性的常見來源。不少組件會依賴於共享狀態的結構,因此若是你須要改變狀態的結構,你就必須修改不少處的邏輯。數據庫

Lenses 讓你可以把狀態的結構抽象,讓它隱藏在 getters 和 setter 以後。爲代碼引入 lens,而不是丟棄你的那些涉及深刻到特定對象結構的代碼庫的代碼。若是後續你須要修改狀態結構,你可使用 lens 來作,而且不須要修改任何依賴於 lens 的代碼。編程

這遵循了需求的小變化將只須要系統的小變化的原則。後端

背景

在 1985 年,「Structure and Interpretation of Computer Programs」 描述了用於分離對象結構與使用對象的代碼的方法的 getter 和 setter 對(下文中稱爲 putget)。文章描述瞭如何建立通用的選擇器,它們訪問複雜變量,但卻不依賴變量的表示方式。這種分離特性很是有用,由於它打破了對狀態結構的依賴。這些 getter 和 setter 對有點像這幾十年來一直存在於關係數據庫中的引用查詢。

Lenses 把 getter 和 setter 對作得更加通用,更有可組合性,從而更加延伸了這個概念。在 Edward Kmett 發佈了爲 Haskell 寫的 Lens 庫後,它們更加普及。他是受到了推論出了遍歷表達了迭代模式的 Jeremy Gibbons 和 Bruno C. d. S. Oliveira,Luke Palmer 的 「accessors」,Twan van Laarhoven 以及 Russell O’Connor 的影響。

注意:一個很容易犯的錯誤是,將函數式 lens 的現代觀念和 Anamorphisms 等同,Anamorphisms 基於 Erik Meijer,Maarten Fokkinga 和 Ross Paterson 1991 年發表的 「使用 Bananas,Lenses,Envelopes 和 Barbed Wire 的函數式編程」。「函數意義上的術語 ‘lens’ 指的是它看起來是總體的一部分。在遞歸結構意義上的術語 ‘lens’ 指的是 [( and )],它在語法上看起來有些像凹透鏡。太長,請不用讀。它們之間並無任何關係。」 ~ Edward Kmett on Stack Overflow

Lens 法則

lens 法則實際上是代數公理,它們確保 lens 能良好運行。

  1. view(lens, set(lens, a, store)) ≡ a — 若是你將一組值設置到一個 store 裏,而且立刻經過 lens 看到了值,你將能獲取到這個被設置的值。
  2. set(lens, b, set(lens, a, store)) ≡ set(lens, b, store) — 若是你爲 a 設置了一個 lens 值,而後立刻爲 b 設置 lens 值,那麼和你只設置了 b 的值的結果是同樣的。
  3. set(lens, view(lens, store), store) ≡ store — 若是你從 store 中獲取 lens 值,而後立刻將這個值再設置回 store 裏,這個值就等於沒有修改過。

在咱們深刻代碼示例以前,記住,若是你在生產環境中使用 lenses,你應該使用通過充分測試的 lens 庫。在 JavaScript 語言中,我知道的最好的是 Ramda。目前,爲了更好的學習,咱們先跳過這部分,本身寫一些原生的 lenses。

// 純函數 view 和 set,它們能夠配合任何 lens 一塊兒使用:
const view = (lens, store) => lens.view(store);
const set = (lens, value, store) => lens.set(value, store);

// 一個將 prop 做爲參數,返回 naive 的函數
// 經過 lens 存取這個 prop。
const lensProp = prop => ({
  view: store => store[prop],
  // 這部分代碼是原生的,它只能爲對象服務:
  set: (value, store) => ({
    ...store,
    [prop]: value
  })
});

// 一個 store 對象的例子。一個可使用 lens 訪問的對象
// 一般被稱爲 「store」 對象
const fooStore = {
  a: 'foo',
  b: 'bar'
};

const aLens = lensProp('a');
const bLens = lensProp('b');

// 使用`view()` 方法來解構 lens 中的屬性 `a` 和 `b`。
const a = view(aLens, fooStore);
const b = view(bLens, fooStore);
console.log(a, b); // 'foo' 'bar'

// 使用 `aLens` 來設置 store 中的值:
const bazStore = set(aLens, 'baz', fooStore);

// 查看新設置的值。
console.log( view(aLens, bazStore) ); // 'baz'
複製代碼

咱們來證明下這些函數的 lens 法則:

const store = fooStore;

{
  // `view(lens, set(lens, value, store))` = `value`
  // 若是你把某個值存入 store,
  // 而後立刻經過 lens 查看這個值,
  // 你將會獲取那個你剛剛存入的值
  const lens = lensProp('a');
  const value = 'baz';

  const a = value;
  const b = view(lens, set(lens, value, store));

  console.log(a, b); // 'baz' 'baz'
}

{
  // set(lens, b, set(lens, a, store)) = set(lens, b, store)
  // 若是你將一個 lens 值存入了 `a` 而後立刻又存入 `b`,
  // 那麼和你直接存入 `b` 是同樣的
  const lens = lensProp('a');

  const a = 'bar';
  const b = 'baz';

  const r1 = set(lens, b, set(lens, a, store));
  const r2 = set(lens, b, store);
  
  console.log(r1, r2); // {a: "baz", b: "bar"} {a: "baz", b: "bar"}
}

{
  // `set(lens, view(lens, store), store)` = `store`
  // 若是你從 store 中獲取到一個 lens 值,而後立刻把這個值
  // 存回到 store,那麼這個值不變
  const lens = lensProp('a');

  const r1 = set(lens, view(lens, store), store);
  const r2 = store;
  
  console.log(r1, r2); // {a: "foo", b: "bar"} {a: "foo", b: "bar"}
}
複製代碼

組合 Lenses

Lenses 是可組合的。當你組合 lenses 的時候,獲得的結果將會深刻對象的字段,穿過全部對象中字段可能的組合路徑。咱們將從 Ramda 引入功能全面的 lensProp 來作說明:

import { compose, lensProp, view } from 'ramda';

const lensProps = [
  'foo',
  'bar',
  1
];

const lenses = lensProps.map(lensProp);
const truth = compose(...lenses);

const obj = {
  foo: {
    bar: [false, true]
  }
};

console.log(
  view(truth, obj)
);
複製代碼

棒極了,可是其實還有不少使用 lenses 的組合值得咱們注意。讓咱們繼續深刻。

Over

在任何仿函數數據類型的狀況下,應用源自 a => b 的函數都是可能的。咱們已經論述了,這個仿函數映射是**可組合的。**相似的,咱們能夠在 lens 中對關注的值應用某個函數。一般狀況下,這個值是同類型的,也是一個源於 a => a 的函數。lens 映射的這個操做在 JavaScript 庫中通常被稱爲 「over」。咱們能夠像這樣建立它:

// over = (lens, f: a => a, store) => store
const over = (lens, f, store) => set(lens, f(view(lens, store)), store);

const uppercase = x => x.toUpperCase();

console.log(
  over(aLens, uppercase, store) // { a: "FOO", b: "bar" }
);
複製代碼

Setter 遵照了仿函數規則:

{ // 若是你經過 lens 映射特定函數
  // store 不變
  const id = x => x;
  const lens = aLens;
  const a = over(lens, id, store);
  const b = store;

  console.log(a, b);
}
複製代碼

對於可組合的示例,咱們將使用一個 over 的 auto-curried 版本:

import { curry } from 'ramda';

const over = curry(
  (lens, f, store) => set(lens, f(view(lens, store)), store)
);
複製代碼

很容易看出,over 操做下的 lenses 依舊遵循仿函數可組合規則:

{ // over(lens, f) after over(lens g)
  // 和 over(lens, compose(f, g)) 是同樣的
  const lens = aLens;

  const store = {
    a: 20
  };

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

  const a = compose(
    over(lens, f),
    over(lens, g)
  );

  const b = over(lens, compose(f, g));

  console.log(
    a(store), // {a: 42}
    b(store)  // {a: 42}
  );
}
複製代碼

咱們目前只基本瞭解了 lenses 的的皮毛,可是對於你繼續開始學習已經足夠了。若是想獲取更多細節,Edward Kmett 在這個話題討論了不少,不少人也寫了許多深度的探索。


Eric Elliott「編寫 JavaScript 應用」(O’Reilly)以及「跟着 Eric Elliott 學 Javascript」 兩書的做者。他爲許多公司和組織做過貢獻,例如 Adobe SystemsZumba FitnessThe Wall Street JournalESPNBBC 等,也是不少機構的頂級藝術家,包括但不限於 UsherFrank Ocean 以及 Metallica

大多數時間,他都在 San Francisco Bay Area,同這世上最美麗的女子在一塊兒。

感謝 JS_Cheerleader

若是發現譯文存在錯誤或其餘須要改進的地方,歡迎到 掘金翻譯計劃 對譯文進行修改並 PR,也可得到相應獎勵積分。文章開頭的 本文永久連接 即爲本文在 GitHub 上的 MarkDown 連接。


掘金翻譯計劃 是一個翻譯優質互聯網技術文章的社區,文章來源爲 掘金 上的英文分享文章。內容覆蓋 AndroidiOS前端後端區塊鏈產品設計人工智能等領域,想要查看更多優質譯文請持續關注 掘金翻譯計劃官方微博知乎專欄

相關文章
相關標籤/搜索