函數式程式設計的核心技巧:如何「表達」資料
前言:不只是寫程式,而是「說故事」
很多人以為函數式程式設計(Functional Programming,簡稱 FP)就是「多用一些 map、filter、reduce」,或是「盡量少用 for 迴圈」。這些觀察沒有錯,但只是表面現象。
FP 真正厲害的地方,其實藏在一個比較容易被忽略的技巧裡:如何表達資料。
聽起來有點抽象對吧?我們慢慢拆解。
什麼叫做「表達資料」?
想像你今天要跟朋友描述一件事情,你可以用兩種方式:
- 口頭轉述:「我昨天去買菜,花了 300 元,買了菜、肉跟水果。」
- 寫成清單:
日期:2026-07-11
項目:菜、肉、水果
總額:300
這兩種方式都能「傳達」同一件事,但清單的版本有一個很大的優勢——它可以被重複使用。你朋友可以拿這張清單去計算月消費、畫成圖表、或是拿去比對別天的紀錄。口頭轉述做不到這些事,因為它只是「說出來」,沒有變成一個可以被「重新處理」的東西。
這就是 FP 想要做的事:把「發生的事情」變成「一份資料」,而不是「一個動作」。
傳統寫法:把邏輯跟資料綁在一起
我們用一個前端工程師常見的例子來說明。假設你要處理一份使用者名單,把每個成年的使用者名字印出來。
用比較「命令式」(imperative)的寫法,你可能會這樣寫:
function printAdultNames(users) {
for (let i = 0; i < users.length; i++) {
if (users[i].age >= 18) {
console.log(users[i].name);
}
}
}
這段程式碼當下看起來沒問題,能跑、能印出結果。但問題是:這段邏輯被寫死在一個函式裡面。
如果明天你的主管跑來跟你說:
「欸,我們現在不要印出來了,改成存到資料庫。」
你就得跑進這個函式裡,把 console.log 改成別的東西。如果後天又要「先過濾成年人,再依照年齡排序,再存起來」呢?你可能又要再改一次這個函式。
問題出在哪? 因為「篩選成年人」這件事,從頭到尾都只存在於這個函式的執行過程中,執行完就消失了。它從來沒有被「表達」成一份獨立的資料,所以你沒辦法把它拿出來重新利用。
FP 的做法:把「意圖」變成資料
函數式程式設計會鼓勵你,把每一個步驟拆解成「純粹的資料轉換」,並且盡量讓每一步都回傳一份新的、乾淨的資料,而不是直接去做某個動作(例如印出來、存檔案)。
同樣的例子,用 FP 的思維改寫:
const isAdult = (user) => user.age >= 18;
const getName = (user) => user.name;
const adultNames = users
.filter(isAdult)
.map(getName);
console.log(adultNames);
這樣寫,表面上看起來只是把 for 迴圈換成 filter 跟 map,但背後的思維差很多:
isAdult是一份「什麼叫做成年」的定義,它不會執行任何動作,只是描述一個條件。getName是一份「如何從使用者資料取出名字」的定義。filter、map只是「套用」這些定義的工具,它們本身不知道,也不在乎最後結果要拿去做什麼。
到了最後一行,adultNames 只是一份單純的陣列(資料),你要拿去 console.log、存資料庫、丟給另一個函式排序,都可以,因為它不帶有任何「已經被使用過」的痕跡。
為什麼這樣做比較好?「當下」與「未來」的差別
回到文章開頭那句話:
資料表達得好,能在「當下」被解讀,也能在「未來」被重新詮釋。
我們拆成兩個層面來看:
1. 當下:程式碼要讓人看得懂
isAdult、getName 這種函式,光看名字就知道它在做什麼,不需要去猜測。相較之下,寫在 for 迴圈裡面的 if (users[i].age >= 18),你得把整段程式讀完,才能拼湊出「喔,原來這裡是在判斷成年」。
表達清楚的資料(或邏輯),可以降低別人(包括未來的你自己)讀懂這段程式碼的成本。
2. 未來:資料要能被重新利用
因為 filter(isAdult) 產生的結果,只是一份普通的陣列,你隨時可以在後面接上新的處理方式:
// 之後想改成排序
const sortedAdultNames = users
.filter(isAdult)
.map(getName)
.sort();
// 或是想改成計算人數
const adultCount = users
.filter(isAdult)
.length;
你完全不需要跑回去修改 isAdult 或 getName 這兩個函式,因為它們從一開始就沒有「綁定」在特定用途上。這就是「未來可以被重新詮釋」的意思——同一份資料轉換邏輯,可以被套用在完全不同的情境裡,不需要重寫。
再深入一點:用「資料」取代「時機」
FP 還有一個更進階但很值得知道的概念:把「什麼時候發生」也變成資料的一部分,而不是寫死在程式流程裡。
舉例來說,一般寫法可能是:
function handleClick() {
fetchUser().then((user) => {
console.log(user.name);
});
}
這段程式碼把「取得使用者」跟「印出名字」焊死在一起了。如果你想在別的地方也用「取得使用者」這件事,但接下來要做的事不一樣,你就得再寫一個新函式。
FP 的思維會傾向把「取得使用者」這個動作本身,也表達成一份「還沒被執行、但描述了要做什麼」的資料(這種東西在 FP 裡常被稱為 描述(description)或是 效果(effect)的表示)。簡單來說就是:
先把「要做的事」寫下來,晚點再決定「什麼時候做、做完要接什麼」。
這部分屬於比較進階的主題(例如 Promise、以及更進階的 Task / IO 概念),這篇文章先點到為止,讓你知道方向即可,未來可以再深入研究。
小結:FP 是一種「延遲決定」的藝術
整理一下這篇文章想傳達的核心概念:
| 傳統寫法 | 函數式寫法 |
|---|---|
| 邏輯跟用途綁死在一起 | 邏輯只描述「規則」,不管用途 |
| 執行完就消失,無法重複利用 | 產出的是資料,隨時可以再加工 |
| 改需求 = 改函式內部 | 改需求 = 重新組合現有的小函式 |
一句話總結:函數式程式設計,是盡量把「要做什麼」寫成一份清楚、乾淨的資料,而不是急著把它「做掉」。 這樣一來,這份資料在當下能被看懂,未來也能被別人(或未來的你)用不同的方式重新解讀、重新組合。
這也是為什麼很多資深工程師會說:「FP 不是一套語法,而是一種思考習慣。」當你開始練習把邏輯拆成小小的、單純的資料轉換,你會發現程式碼變得更容易維護,也更容易應付需求的變化。
延伸練習建議:下次寫程式時,試著問自己一句話——「這段邏輯,未來還能不能被別的地方重複使用?」如果答案是不行,通常代表它把「資料」跟「用途」綁得太緊了,可以試著拆開來看看。