Redux概念之四: reducer(歸納函式)與純函式

eyesofkids發表於2017-01-24

reducer(歸納函式)

reducer(歸納函式)這種函式的名稱,是由陣列的一個迭代方法reduce(歸納)而來,你可以參考MDN中的相關說明:

在JS語言中的陣列reduce(歸納)這個方法是一種應用於特殊情況的迭代方法,它可以藉由一個回撥(callback)函式,來作前後值兩相運算,然後不斷縮減陣列中的成員數量,最終返回一個值。reduce(歸納)並不會更動作為傳入的陣列(呼叫reduce的陣列),所以它也沒有副作用。一個簡單的例子如下:

const aArray = [0, 1, 2, 3, 4, 5]

const sum = aArray.reduce(function(pValue, value, index, array){
    return pValue + value
})

console.log(sum) // 15

陣列的reduce(歸納)方法,還有另一種語法樣式,是帶有初始值的,這會比較接近Redux中的reducer樣式,如下面的例子:

const initialState = 0

const sum = [1, 2, 3, 4, 5].reduce(add, initialState)

function add(a, b) {
    // `a` 代表前一個狀態
    // `b` 代表目前在陣列中的專案
    return a + b
}

console.log(sum) // 15

reduce(歸納)方法具有分散運算的特點,常見於下面幾種應用之中:

  • 兩相比較最後取出特定的值(最大或最小值)

  • 計算所有成員(值),總合或相乘

  • 其它需要兩兩處理的情況(組合巢狀陣列等等)

不過,Redux中的reducer與陣列中的reduce方法並不相同,其中最大的差異,是reducer並不是對一整個列表進行歸納運算,而是對一個action(動作)與目前的state進行歸納運算,回傳出新的state。

副作用與純函式

當一個函式是純函式時,我們可以說輸出只取決於輸入

對於函式來說,具有副作用代表著可能會更動到外部環境,或是更動到傳入的引數值。函式的區分是以 純(pure)函式 與 不純(impure)函式 兩者來區分,但這不光只有無副作用的差異,還有其他的條件。純函式(pure function)即滿足以下三個條件的函式,以下的定義是來自於Redux的概念:

  • 給定相同的輸入(傳入值),一定會返回相同輸出值結果(返回值)

  • 不會產生副作用

  • 不依賴任何外部的狀態

一個典型的純函式的例子如下:

const sum = function(value1, value2) {
  return value1 + value2
}

套用上面說的條件定義,你可以用下面觀察來理解它是不是一個純函式:

  • 只要每次給定相同的輸入值,就一定會得到相同的輸出值: 例如傳入1與2,就一定會得到3

  • 不會改變原始輸入引數,或是外部的環境,所以沒有副作用

  • 不依頼其他外部的狀態,變數或常量

那什麼又是一個不純的函式?看以下的例子就是,它需要依賴外部的狀態/變數值:

let count = 1

let increaseAge = function(value) {
  return count += value
}

在JavaScript中不純函式很常見,像我們一直用來作為輸出的console.log函式,或是你可能會在很多例子看到的alert函式,都是”不”純函式,這類函式通常沒有返回值,都是用來作某件事,像console.log會更動瀏覽器的主控臺(外部環境)的輸出,也算是一種副作用。

每次輸出值都不同的不純函式一類,最典型的就是Math.random,這是產生隨機值的內建函式,既然是隨機值當然每次執行的返回值都不一樣。

例如在陣列的內建方法中,有一些是有副作用,而有一些是無副作用的,這個部份需要查對應API才能夠清楚。不會改變傳入的陣列的,會在作完某件事後返回一個新陣列的方法,就是無副作用的純函式(方法),而會改變原陣列就算是不純函式(方法)了。

下面是兩個在陣列中作同樣事情的不同方法,都是要取出只包含陣列的前三個成員的陣列。一個用splice,另一用是slice,看起來都很像,連這兩個方法的名稱都很像,但卻是完全屬於不同的種類:

// 不純粹(impure),splice會改變到原陣列
const firstThree = function(arr) {
  return arr.splice(0,3)
}

// 純粹(pure),slice會返回新陣列
const firstThree = function(arr) {
  return arr.slice(0,3)
}

其他有許多內建的或常用的函式都是免不了有副作用的,例如這些應用:

  • 會改變傳參(物件、陣列)的函式(方法)

  • 時間性質的函式,setTimeout等等

  • I/O相關

  • 資料庫相關

  • AJAX

純函式當然有它的特別的優點:

  • 程式碼閱讀性提高

  • 較為封閉與固定,可重覆使用性高

  • 輸出輸入單純,易於測試、除錯

  • 因為輸入->輸出結果固定,可以快取或作記憶處理,在高花費的應用中可作提高執行效率的機制

最後,並不是說有副作用的函式就不要使用,而且要很清楚的理解這個概念,然後儘可能在你自己的撰寫的一般功能函式上使用純函式,以及讓必要有副作用的函式得到良好的管控。現在已經有一些新式的函式庫或框架(例如Redux),會特別強制要求在某些地方只能使用純函式,而具有副作用的不純函式只能在特定的情況下才能使用。

注: 雖然在副作用與純函式的介紹中,我們有提到一些呼叫外部API(console.log/alert)、時間(Date())、隨機(Math.random)也屬於有副作用的呼叫,但以等級來區分它們只算是”輕度”或”微量”的副作用,這些在reducer或Action Creators能不能用?答案是可以用但也不要用,它會影響到純函式的一些優化。以在副作用的主題來說,非同步執行才是”中度”或”一般”等級的副作用,我們談到副作用通常是指這個等級的。當然也有”重度”等級的副作用,那是另一個層次的特殊應用情況討論,例如組合出來的複雜非同步執行流程結構。

相關文章