函式程式設計 (functional programming) — 宣告式 (declarative)

2025年1月9日

💎 加入 E+ 成長計畫 如果你喜歡我們的內容,歡迎加入 E+,獲得更多深入的軟體前後端內容

函式程式設計 (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+ 的詳細介紹)。

🧵 如果你想收到最即時的內容更新,可以在 FacebookInstagram 上追蹤我們