- 原文地址:Lenses: Composable Getters and Setters for Functional Programming
- 原文做者:Eric Elliott
- 譯文出自:掘金翻譯計劃
- 本文永久連接:github.com/xitu/gold-m…
- 譯者:EmilyQiRabbit
- 校對者:Moonliujk
注意:本篇是「組合軟件」這本書 的一部分,它將以系列博客的形式展開新生。它涵蓋了 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),表明了一個包含 x
、y
和 z
三點的座標: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 讓你可以把狀態的結構抽象,讓它隱藏在 getters 和 setter 以後。爲代碼引入 lens,而不是丟棄你的那些涉及深刻到特定對象結構的代碼庫的代碼。若是後續你須要修改狀態結構,你可使用 lens 來作,而且不須要修改任何依賴於 lens 的代碼。編程
這遵循了需求的小變化將只須要系統的小變化的原則。後端
在 1985 年,「Structure and Interpretation of Computer Programs」 描述了用於分離對象結構與使用對象的代碼的方法的 getter 和 setter 對(下文中稱爲 put
和 get
)。文章描述瞭如何建立通用的選擇器,它們訪問複雜變量,但卻不依賴變量的表示方式。這種分離特性很是有用,由於它打破了對狀態結構的依賴。這些 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 能良好運行。
view(lens, set(lens, a, store)) ≡ a
— 若是你將一組值設置到一個 store 裏,而且立刻經過 lens 看到了值,你將能獲取到這個被設置的值。set(lens, b, set(lens, a, store)) ≡ set(lens, b, store)
— 若是你爲 a
設置了一個 lens 值,而後立刻爲 b
設置 lens 值,那麼和你只設置了 b
的值的結果是同樣的。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 的時候,獲得的結果將會深刻對象的字段,穿過全部對象中字段可能的組合路徑。咱們將從 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 的組合值得咱們注意。讓咱們繼續深刻。
在任何仿函數數據類型的狀況下,應用源自 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 Systems、Zumba Fitness、The Wall Street Journal、ESPN 和 BBC 等,也是不少機構的頂級藝術家,包括但不限於 Usher、Frank Ocean 以及 Metallica。
大多數時間,他都在 San Francisco Bay Area,同這世上最美麗的女子在一塊兒。
感謝 JS_Cheerleader。
若是發現譯文存在錯誤或其餘須要改進的地方,歡迎到 掘金翻譯計劃 對譯文進行修改並 PR,也可得到相應獎勵積分。文章開頭的 本文永久連接 即爲本文在 GitHub 上的 MarkDown 連接。
掘金翻譯計劃 是一個翻譯優質互聯網技術文章的社區,文章來源爲 掘金 上的英文分享文章。內容覆蓋 Android、iOS、前端、後端、區塊鏈、產品、設計、人工智能等領域,想要查看更多優質譯文請持續關注 掘金翻譯計劃、官方微博、知乎專欄。