函式程式设计 (functional programming) — 宣告式 (declarative)

函式程式设计 (functional programming) — 宣告式 (declarative)

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

相关数据