[譯]藉助函式完成可組合的資料型別(軟體編寫)(第十部分)

吳曉軍發表於2017-10-15

藉助函式完成可組合的資料型別(軟體編寫)(第十部分)

Smoke Art Cubes to Smoke — MattysFlicks — (CC BY 2.0)
Smoke Art Cubes to Smoke — MattysFlicks — (CC BY 2.0)

(譯註:該圖是用 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 步能讓測試通過:

  1. 將函式 fn 變為 add 函式,該函式返回 t(value + n)n 表示傳入引數。
  2. 為函式 t 新增一個 .valueOf() 方法,使得新的 add() 函式能夠接受 t() 返回的例項作為引數。 + 運算子會使用 n.valueOf() 的結果作為第二個運算元。
  3. 使用 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 SystemsZumba FitnessThe Wall Street JournalESPNBBC 等 , 也是很多機構的頂級藝術家,包括但不限於 UsherFrank Ocean 以及 Metallica

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


掘金翻譯計劃 是一個翻譯優質網際網路技術文章的社群,文章來源為 掘金 上的英文分享文章。內容覆蓋 AndroidiOSReact前端後端產品設計 等領域,想要檢視更多優質譯文請持續關注 掘金翻譯計劃官方微博知乎專欄

相關文章