Pure Function 的定義

一個函式是 pure 的,當且僅當:

  1. Deterministic:相同輸入永遠得到相同輸出
  2. 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 執行),也讓人更容易推理程式的行為——不需要追蹤「執行這個函式時外部狀態是什麼」。