函式程式设计 (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+ 的详细介绍)。