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

LeviDing發表於2019-01-15

注意:本篇是“組合軟體”這本書 的一部分,它將以系列部落格的形式展開新生。它涵蓋了 JavaScript(ES6+)函數語言程式設計和可組合軟體技術的最基礎的知識。<
上一篇
| <
<
從第一部分開始

lens 是一對可組合的 getter 和 setter 純函式,它會關注物件內部的一個特殊欄位,並且會遵從一系列名為 lens 法則的公理。將物件視為整體,欄位視為區域性。getter 以物件整體作為引數,然後返回 lens 所關注的物件的一部分。

// view = whole =>
part複製程式碼

setter 則以物件整體作為引數,以及一個需要設定的值,然後返回一個新的物件整體,這個物件的特定部分已經更新。和一個簡單設定物件成員欄位的值的函式不同,Lens 的 setter 是純函式:

// set = whole =>
part =>
whole複製程式碼

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

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

[x, y, z]複製程式碼

為了能分別獲取或者設定每個欄位,你可以建立三個 lenses。每個軸一個。你可以手動建立關注每個欄位的 getter:

const getX = ([x]) =>
x;
const getY = ([x, y]) =>
y;
const getZ = ([x, y, z]) =>
z;
console.log( getZ([10, 10, 100]) // 100);
複製程式碼

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

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) =>
storeconst 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前端後端區塊鏈產品設計人工智慧等領域,想要檢視更多優質譯文請持續關注 掘金翻譯計劃官方微博知乎專欄

來源:https://juejin.im/post/5c3d35f8f265da611e4de068

相關文章