- 原文地址:Composable Datatypes with Functions
- 原文作者:
Eric Elliott- 譯文出自:掘金翻譯計劃
- 本文永久連結:github.com/xitu/gold-m…
- 譯者:yoyoyohamapi
- 校對者:IridescentMia lampui
藉助函式完成可組合的資料型別(軟體編寫)(第十部分)
(譯註:該圖是用 PS 將煙霧處理成方塊狀後得到的效果,參見 flickr。)
注意:這是 “軟體編寫” 系列文章的第十部分,該系列主要闡述如何在 JavaScript ES6+ 中從零開始學習函數語言程式設計和組合化軟體(compositional software)技術(譯註:關於軟體可組合性的概念,參見維基百科 Composability)。後續還有更多精彩內容,敬請期待!
<上一篇 | << 返回第一章
在 JavaScript 中,最簡單的方式完成組合就是函式組合,並且一個函式只是一個你能夠為之新增方法的物件。換言之,你可以這麼做:
const t = value => {
const fn = () => value;
fn.toString = () => `t(${ value })`;
return fn;
};
const someValue = t(2);
console.log(
someValue.toString() // "t(2)"
);複製程式碼
這是一個返回數字型別例項的工廠函式 t
。但是要注意,這些例項不是簡單的物件,它們是函式,並且是可組合的函式。假定我們使用 t()
來完成求和任務,那麼當我們組合若干個函式 t()
來求和也就是合情合理的。
首先,假定我們為 t()
確立了一些規則(====
意味著 “等於”):
t(x)(t(0)) ==== t(x)
t(x)(t(1)) ==== t(x + 1)
在 JavaScript 中,你也可以通過我們建立好的 .toString()
方法進行比較:
t(x)(t(0)).toString() === t(x).toString()
t(x)(t(1)).toString() === t(x + 1).toString()
我們也能將上述程式碼翻譯為一種簡單的單元測試:
const assert = {
same: (actual, expected, msg) => {
if (actual.toString() !== expected.toString()) {
throw new Error(`NOT OK: ${ msg }
Expected: ${ expected }
Actual: ${ actual }
`);
}
console.log(`OK: ${ msg }`);
}
};
{
const msg = 'a value t(x) composed with t(0) ==== t(x)';
const x = 20;
const a = t(x)(t(0));
const b = t(x);
assert.same(a, b, msg);
}
{
const msg = 'a value t(x) composed with t(1) ==== t(x + 1)';
const x = 20;
const a = t(x)(t(1));
const b = t(x + 1);
assert.same(a, b, msg);
}複製程式碼
起初,測試會失敗:
NOT OK: a value t(x) composed with t(0) ==== t(x)
Expected: t(20)
Actual: 20複製程式碼
但是我們經過下面 3 步能讓測試通過:
- 將函式
fn
變為add
函式,該函式返回t(value + n)
,n
表示傳入引數。 - 為函式
t
新增一個.valueOf()
方法,使得新的add()
函式能夠接受t()
返回的例項作為引數。+
運算子會使用n.valueOf()
的結果作為第二個運算元。 - 使用
Object.assign()
將toString()
,.valueOf()
方法分配給add()
函式
將 1 至 3 步綜合起來得到:
const t = value => {
const add = n => t(value + n);
return Object.assign(add, {
toString: () => `t(${ value })`,
valueOf: () => value
});
};複製程式碼
之後,測試便能通過:
"OK: a value t(x) composed with t(0) ==== t(x)"
"OK: a value t(x) composed with t(1) ==== t(x + 1)"複製程式碼
現在,你可以使用函式組合來組合 t() ,從而達到求和任務:
// 自頂向下的函式組合:
const pipe = (...fns) => x => fns.reduce((y, f) => f(y), x);
// 求和函式為 pipeline 傳入需要的初始值
// curry 化的 pipeline 複用度更好,我們可以延遲傳入任意的初始值
const sumT = (...fns) => pipe(...fns)(t(0));
sumT(
t(2),
t(4),
t(-1)
).valueOf(); // 5複製程式碼
任何資料型別都適用
無論你的資料形態是什麼樣子的,只要它存在有意義的組合操作,上面的策略都能幫到你。對於列表或者字串來說,組合能夠完成連線操作。對於 DSP(數字訊號處理)來說,組合完成的就是訊號的求和。當然,其他的操作也能為你帶來想要的結果。那麼問題來了,哪種操作最能反映組合的觀念?換言之,哪種操作能更受益於下面的程式碼組織方式:
const result = compose(
value1,
value2,
value3
);複製程式碼
可組合的貨幣
Moneysafe 是一個實現了這個可組合的、函式式資料型別風格的開源庫。JavaScript 的 Number
型別無法精確地表示美分的計算:
.1 + .2 === .3 // false複製程式碼
Moneysafe 通過將美元型別提升為美分型別解決了這個問題:
npm install --save moneysafe複製程式碼
之後:
import { $ } from 'moneysafe';
$(.1) + $(.2) === $(.3).cents; // true複製程式碼
ledger 語法利用了 Moneysafe 將一般的值提升為可組合函式的優勢。它暴露一個簡單的、稱之為 ledger 的函式組合套件:
import { $ } from 'moneysafe';
import { $$, subtractPercent, addPercent } from 'moneysafe/ledger';
$$(
$(40),
$(60),
// 減去折扣
subtractPercent(20),
// 上稅
addPercent(10)
).$; // 88複製程式碼
該函式的返回值型別是提升後 money 型別。該返回值暴露一個 .$
getter 方法,這個 getter 能夠將內部的浮點美分值四捨五入為美元。
該結果是執行 ledger 風格的金幣計算一個直觀反映。
測試一下你是否真的懂了
克隆 Moneysafe 倉庫:
git clone git@github.com:ericelliott/moneysafe.git複製程式碼
執行安裝過程:
npm install複製程式碼
執行單元測試,監控控制檯輸出。所有的用例都會通過:
npm run watch複製程式碼
開啟一個新的終端,刪除 moneysafe 的實現:
rm source/moneysafe.js && touch source/moneysafe.js複製程式碼
回到之前的終端視窗,你將會看到一個錯誤。
你現在的任務是利用單元測試輸出及文件的幫助,從頭實現 moneysafe.js
並通過所有測試。
下一篇: JavaScript Monads 讓一切變得簡單 >
接下來
想學習更多 JavaScript 函數語言程式設計嗎?
跟著 Eric Elliott 學 Javacript,機不可失時不再來!
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,同這世上最美麗的女子在一起。
掘金翻譯計劃 是一個翻譯優質網際網路技術文章的社群,文章來源為 掘金 上的英文分享文章。內容覆蓋 Android、iOS、React、前端、後端、產品、設計 等領域,想要檢視更多優質譯文請持續關注 掘金翻譯計劃、官方微博、知乎專欄。