純函式(Pure Funciton)在 FP 中是一個很核心的概念,在 FP 這個設計典範中,我們都會使用純函式的方式來撰寫我們的程式碼。
首先讓我們來看看純函式的定義:
那我們為什麼在 FP 中要使用純函式呢?其實道理與先前介紹的 Immutable Data 雷同,我們希望可以讓程式碼的狀態不互相依賴,以免造成變數與變數間互相覆蓋的狀況。
更重要的是,我們同時也希望程式碼的覆用性可以更高,而不是在同個函式中,同時處理不同性質的任務,一旦這些任務重複性出現在不同的流程中,就會發現任務彼此之間過於耦合難以拆分,要擴充新的功能,就只能向樂高一樣繼續往上疊,或一改就要改到天荒地老的窘境。
除了開發過程可能變得沒有效率外,把程式碼當樂高疊的後果是,萬一未來有人不小心改到了某個地方,就很容易導致這個程式碼樂高塔輕易崩塌。
在 FP 中,我們傾向把任務拆分成最小最小的單位,並且透過函式來處理這些任務,而規則單純卻又嚴謹的純函式就成為 FP 最萬用的工具。
在這一章節中我們主要會針對純函式中「單輸入單輸出」的概念來進行討論,而副作用的部分我們在之後的章節還會進行更深入的討論。
如果要用一句話概括純函式的精髓的話,大概就是單輸入單輸出了,這句話的意涵是指:同樣的參數會獲得同樣的回傳值。
這是什麼意思呢?
用數學來看的話,則是映射的概念,當我們丟了一個代數進函式,最後一定會產出相對應的解,而這個解是固定的值。
老實說,在我第一次聽到純函式時,我也稍微看了一下大家對這個詞的定義,但還是沒辦法深刻理解其中應用。
後來我發現,對於剛接觸程式設計典範的新手來說,光是上方所提到兩點概括式定義可能沒辦法具象化純函式所要解決的問題。
如果我們把純函式的定義再拆解的具體一點的話,可以分為以下幾點:
接著就讓我們根據以上幾點進行更深入的討論吧!
試想看看如果我們現在有一個情境,果園的農夫正在收成,由於水果採摘下來會因為各種因素導致有 1 成的水果無法販賣,所以我們函式需要一個函式將農夫收成的所有品項乘以 0.9 才是會是倉庫中最終的庫存量,此時我們可能會有哪些作法呢?
在初學程式語言時,我們可能會這樣寫:
const appleAmount = 10;
let appleStockAmount;
function calcAppleStockAmount () {
appleStockAmount = appleAmount * 0.9;
}
calcAppleStockAmount();
// 9
哇,看起來都正確耶!很棒很棒,那就決定這樣寫了!
但過了幾天你可能會發現一個狀況:農夫的果園不只有種蘋果,還有其他種作物要收成,按照目前的邏輯,程式碼可能會變成好幾倍,於是我們決定要省去那些重複的邏輯,讓所有的水果共用一個函式:
const appleAmount = 10;
const orangeAmount = 20
let appleStockAmount;
let orangeStockAmount;
function calcFruitStockAmount () {
appleStockAmount = appleAmount * 0.9;
orangeStockAmount = orangeAmount * 0.9;
}
calcFruitStockAmount();
// appleStockAmount -> 9
// orangeStockAmount -> 18
於是乎計算水果庫存的函式就這麼誕生了!
然而過了幾天狀況又發生了,此時蘋果因為收成良好,所以沒有任何折損,所以當前的採收量就會等於庫存量,但是等等⋯⋯即便我不想改變蘋果的庫存量,我的函式這麼設計一定會改變到蘋果的庫存量耶?
再往後續思考,萬一我的折損比率是浮動的,那我不是每次都要修改程式嗎?到底要怎麼撰寫才能讓 calcFruitStockAmount
函式的效益最大化呢?
此時純函式該登場了,讓我們在函式中導入 Pure 的概念吧!
還記得以上四點純函式的特點嗎?讓我們將上述概念一一套用進我們的範例函式中,首先讓我們看看有哪邊是可以透過帶入參數改善的:
let appleStockAmount;
let orangeStockAmount;
function calcFruitStockAmount (appleAmount, orangeAmount) {
appleStockAmount = appleAmount * 0.9;
orangeStockAmount = orangeAmount * 0.9;
}
calcFruitStockAmount(10, 20);
// appleStockAmount -> 9
// orangeStockAmount -> 18
此時我們會發現,光是把水果的採收量代換成參數,程式碼就少了兩行,但這邊依然有個問題存在是:
不論農夫採收了多少種水果,都應該有辦法透過 calcFruitStockAmount
函式來進行自動化的計算,且不能影響到其他種水果的庫存量。
還記得純函式其中一個守則「函式本身不會對其他變數造成任何改變」嗎?為什麼我們要遵守這個守則?
試想上面範例程式碼的情境:即便我們把部分的變數改為使用參數帶入,但依然沒有解決水果不一定每次收成都要計算折損的問題。
因此所我們不應該把所有可能要計算的水果種類在一開始就寫死,也就是不要把我們未來會遇到的情境想的太狹隘、限制在指定的那幾種情境,而是在是要計算時才給定要計算的水果種類名稱,甚至是要計算時才依當時狀況給定折損率。
這時候考驗對於 JavaScript 函式理解度的時候到了,還記得函式也可以作為變數的值嗎?經過再次優化的程式碼會長成:
const calcFruitStockAmount = (fruitAmount, rate) => fruitAmount * rate;
const appleStockAmount = calcFruitStockAmount(10, 1);
const orangeStockAmount = calcFruitStockAmount(20, 0.9);
// appleStockAmount -> 10
// orangeStockAmount -> 18
比起最初的程式碼,我們會發現,經過修改的 calcFruitStockAmount
完全符合:
this.rate
的自有狀態保留在函式中看到這裡你可能會發現純函式就是一個表達式(Expression),而且是一個規則很多、嚴謹的表達式。
還記得在之前討論函式是第一等公民章節中我們所提到這個範例嗎?
const box = {
banana: 0,
addBanana: function(){ return this.banana+=1;},
}
box.addBanana();
// -> 1
了解純函式的運作後,會發現我們實際上並不是真的要一個計算香蕉數量的韓式,而是某種東西的累加器,我們甚至不想要讓這個函式有自己狀態,例如:仰賴 this
關鍵字,純函式中基本上是看不到的,所以讓我們來稍微改一下上方的程式碼:
const accumulator = (prev, count) => prev + count;
const currentBanana = accumulator(0, 5);
// -> 5
未來不論是要累加什麼東西的數量,我們都可以應用這個累加器了!
說到這,會發現要在程式碼中導入純函式的概念,對一開始寫程式的人來說可能是很難快速掌握的,在下一章節中,我們要聊聊另外一個對於學習純函式有幫助的概念:抽象化。