Pure Function 的定義
一個函式是 pure 的,當且僅當:
- Deterministic:相同輸入永遠得到相同輸出
- No Side Effect:不修改外部狀態(不改 global variable、不讀寫 DB、不發 HTTP request、不寫 log)
// Pure function
function add(a, b) {
return a + b;
}
// Impure:結果依賴外部狀態
let tax = 0.1;
function calculatePrice(price) {
return price * (1 + tax); // tax 可能被別人修改
}
// Impure:有 side effect(修改外部狀態)
let totalRevenue = 0;
function recordSale(amount) {
totalRevenue += amount; // 修改外部變數
return amount;
}
// Impure:有 side effect(I/O)
function fetchUser(id) {
return db.query(`SELECT * FROM users WHERE id = ${id}`); // 依賴外部 I/O
}為什麼 Pure Function 好測試
// Pure function 的測試:只需要輸入和輸出
test('add 2 and 3', () => {
expect(add(2, 3)).toBe(5); // 不需要 setup、不需要 teardown
});
// Impure function 的測試:需要 mock 外部依賴
test('calculatePrice', () => {
tax = 0.1; // 需要設置全局狀態
expect(calculatePrice(100)).toBe(110);
tax = 0; // 需要清理,不然影響其他測試
});Pure function 的測試:不需要 beforeEach、不需要 mock、不需要 teardown——只是「輸入這個,輸出那個」。測試速度快,測試隔離好,不會有測試順序的依賴問題。
Side Effect 不是壞的,是需要管理的
程式最終需要做 side effect——讀寫 DB、發 HTTP request、渲染畫面。這些是程式存在的意義。
函數式程式設計的策略:把 side effect 推到系統的邊界,讓業務邏輯盡量是 pure function。
// 核心邏輯是 pure function
function calculateDiscount(order, user) {
if (user.isPremium && order.total > 100) {
return order.total * 0.9;
}
return order.total;
}
// Side effect 在邊界
async function processOrder(orderId) {
const order = await db.getOrder(orderId); // side effect(讀 DB)
const user = await db.getUser(order.userId); // side effect(讀 DB)
const finalPrice = calculateDiscount(order, user); // pure,可以單獨測試
await db.updateOrder(orderId, { price: finalPrice }); // side effect(寫 DB)
await mailer.sendConfirmation(order); // side effect(發 mail)
}這樣的結構讓 calculateDiscount 可以被單獨的快速 unit test 覆蓋,processOrder 的 integration test 只需要驗證 side effect 的組合邏輯。
Referential Transparency
Pure function 的另一個表述:函式呼叫可以被其回傳值取代,程式行為不變。
// add(2, 3) 可以被 5 取代,程式行為一樣
const x = add(2, 3) + add(1, 4);
// 等同於
const x = 5 + 5;
// 等同於
const x = 10;這個性質讓 compiler 可以做優化(記憶化、reorder 執行),也讓人更容易推理程式的行為——不需要追蹤「執行這個函式時外部狀態是什麼」。