Function Composition and Pointfree Coding Style

Pointfree 代碼風格透過函式組合提昇代碼品質

什麼是 Pointfree?

一種代碼風格,著重於函式的組合關係而非具體資料,定義函式時不顯式提其參數(points),而是透過函式組合與高階函式來表達資料流動

平方的總和

sumOfSquares([1,2,3]) // (1 * 1) + (2 * 2) + (3 * 3) = 14

Pointful 案例

  1. 接收數字陣列
  2. 遍歷每個數字取其平方
  3. 加總平方數字為總數
  4. 返回總數
function sumOfSquares(arr) {
let sum = 0
for (let i = 0; i < arr.length; i++) {
sum += arr[i] * arr[i]
}
return sum
}

Pointfree 案例

透過組合 JS 內建高階函式的方法 mapreduce 達成函數式的解決方案:

const square = x => x * x;
const add = (a, b) => a + b;
const sumOfSquares = arr => arr
.map(square)
.reduce(add, 0);

不過嚴格來說這還不完全是 Pointfree,因為我們仍然在函式中直接操作 arr,可以將「資料操作」抽離成函式之間的組合,也就是透過 compose 來達成:

const compose = (...fns) => x => fns.reduceRight((v, f) => f(v), x);
const map = fn => arr => arr.map(fn);
const reduce = (fn, init) => arr => arr.reduce(fn, init);
const square = x => x * x;
const add = (a, b) => a + b;
const sumOfSquares = compose(
reduce(add, 0),
map(square)
);
sumOfSquares([1, 2, 3]);

取出欄位

假設有一群使用者資料,要取出所有使用者的名字欄位:

const users = [
{ id: 1, name: "Tom" },
{ id: 2, name: "Mary" },
{ id: 3, name: "John" }
];

Pointful 案例

  1. 接收使用者陣列
  2. 建立一個新的空陣列
  3. 遍歷每個使用者物件,取出 name 加進新陣列
  4. 返回新陣列
function getNames(users) {
const names = []
for (let i = 0; i < users.length; i++) {
names.push(users[i].name)
}
return names
}
getNames(users) // ["Tom", "Mary", "John"]

Pointfree 範例

同樣地也可以用內建的高階函式來讓程式更簡潔,並避免顯式操作參數:

const prop = key => obj => obj[key];
const getNames = users => users.map(prop("name"));
getNames(users) // ["Tom", "Mary", "John"]

這裡我們再將 map 包裝成可組合的版本,讓它能直接接收一個函式並返回新的處理函式。

const map = fn => arr => arr.map(fn);
const prop = key => obj => obj[key];
const getNames = map(prop("name"));
getNames([
{ name: "Tom" },
{ name: "Mary" },
{ name: "John" },
]); // ["Tom", "Mary", "John"]

總結

  • Pointful:函式明確地接收並操作資料。
  • Pointfree:透過函式組合表達資料流,不直接提及資料本身。

好處

  1. 減少不必要的命名保持代碼簡潔
  2. 達成更好的通用組合性

壞處

  1. 需要額外理解函式組合與柯里化概念,對初學者不太直覺。

延伸閱讀