函式程式設計 (functional programming) — 宣告式 (declarative)
2025年1月9日
函式程式設計 (functional programming) 是近年來在前後端界相當流行的程式撰寫方式。舉例來說,社群最熱門的前端套件 React 就大量使用了函式程式設計的概念。因此,不論你是前端或後端工程師,了解函式程式設計概念,對於寫程式的理解都會有所提升。
因此,接下來我們會有幾篇談函式程式設計的內容,帶著讀者們一起理解函式程式設計,以及如何在工作上使用上函式程式設計。在這一篇,我們會先從函式程式設計的一些基礎概念談起。
用宣告式 (declarative) 的方式寫程式
函式程式設計顧名思義,是用函式來寫程式 (對比起物件導向程式設計會以物件為主軸)。以函式為導向的一個特點是可以讓開發者用宣告式 (declarative) 的方式寫程式。
這是什麼意思呢? 讓我們用一個具體例子來說明。
假如今天想要把某一個陣列的每個數字,都乘上 2 然後放到一個新的陣列中,一般來說可以這樣寫。如下面的程式碼,我們可以有一個 for
迴圈,然後迭代原本的 numbers
陣列,然後把每個元素 *2
後加進去 doubled
陣列。
const doubled = [];
for (let i = 0; i < numbers.length; i++) {
doubled.push(numbers[i] * 2);
}
但假如要用函式導向的寫法,我們可以利用多數程式語言有內建的 map
方法,改寫成下面這樣
const doubled = numbers.map((x) => x * 2);
上面這兩種程式的寫法,分別叫命令式 (imperative) 與宣告式 (declarative)。
上面第一種用 for
的命令式寫法,就像下不同的指令一樣著重在如何 (how) 執行,所以會詳細地寫出每個步驟與細節。這樣做的好處,是寫程式的人能夠掌握每個細節。不過,讀程式碼的人,需要完整看完才能夠理解程式碼在做什麼。
而用 map
的宣告式寫法,則是著重在要什麼 (what),會把實作的細節隱藏起來,讓程式碼可以更簡潔好讀。以 map
來說,就把迭代過陣列的實作細節隱藏起來,讓讀程式碼的人可以專注在核心邏輯的部分 (以上面來說是 x * 2
)。
如果用一個複雜一點的例子,我們能夠清楚理解兩者的差別。假如我們有下面的電商購物車資料
const cartItems = [
{ id: 1, name: "iPhone", price: 30000, quantity: 1, inStock: true },
{ id: 2, name: "AirPods", price: 5000, quantity: 2, inStock: true },
{ id: 3, name: "充電器", price: 900, quantity: 3, inStock: false },
{ id: 4, name: "保護殼", price: 1500, quantity: 1, inStock: true },
];
下面這段程式碼,可能沒辦法一眼看出在做什麼
const processCartImperative = (items) => {
const result = {
items: [],
totalQuantity: 0,
totalAmount: 0,
};
for (let i = 0; i < items.length; i++) {
const item = items[i];
if (item.inStock) {
const subtotal = item.price * item.quantity;
result.items.push({
name: item.name,
subtotal: subtotal,
quantity: item.quantity,
});
result.totalQuantity += item.quantity;
result.totalAmount += subtotal;
}
}
return result;
};
但同樣的程式碼,如果用函式程式設計的寫法,就能夠一目瞭然。可以看到事先篩選出有庫存的商品,然後計算每個商品的小計、最後輸出購物車的商品、總數量、總金額
const processCartFunctional = (items) => {
const inStockItems = items
.filter((item) => item.inStock)
.map((item) => ({
name: item.name,
subtotal: item.price * item.quantity,
quantity: item.quantity,
}));
return {
items: inStockItems,
totalQuantity: inStockItems.reduce((sum, item) => sum + item.quantity, 0),
totalAmount: inStockItems.reduce((sum, item) => sum + item.subtotal, 0),
};
};
希望透過上面的例子,可以看到函式程式設計這種宣告式寫法,對於程式碼可讀與可維護性的好處。當然,不是所有時候都適合用宣告式,例如在需要精準控制記憶體或效能的場景,命令式的寫法可能更適合。但在要專注在商業邏輯的場景,就特別適合用這種方式寫。
閱讀更多
如果你對「函式程式設計」這主題感興趣,我們在 E+ 有更深入的討論。有興趣的讀者,歡迎加入 E+ 成長計畫。我們在 E+ 有更深入的內容,談到純函式 (Pure Function) 是什麼? 為什麼要純函式? 副作用 (side effects) 是什麼? 為什麼要盡量避免副作用? 等不同議題
本文為 E+ 成長計畫的深度內容,截取段落開放免費閱讀。歡迎加入 E+ 成長計畫閱讀完整版本 (點此了解 E+ 的詳細介紹)。