yo! what's up!
本篇文章會簡單地介紹基本的 Functional Programming 概念,這些概念不僅重要,更是貫穿了之後的主題。
給定相同的輸入,一定會得到相同的輸出。並且不會產生任何 Side Effect.
那我們就來探討一下這段純函式(pure function)的定義,
舉例來說, Math.random
Math.random() // 0.06243529219776711
Math.random() // 0.9436551613058293
Math.random() // 0.29082104009990295
Math.random
有什麼問題呢?可以看到給定同樣的輸入,卻是不同的輸出,這就是不純的函式(impure 函式)。
滿足純函式的要點之一,就是必須是在相同輸入下,無論重複呼叫多少次,輸出的結果永遠是一樣的,舉 square square為例,只要給定特定的值,就會回傳該值的平方。
const square = num => Math.pow(num, 2);
square(2) // 4
square(2) // 4
square(2) // 4
數學中的函式就像是兩個集合的對應關係,且集合內的數皆可以在另外一個集合找到對應的唯一值。而純函式就是數學中的函式,必須是一對一或是多對一的關係。
Side Effect 就是當呼叫函式時也改變外部物件的狀態,我們就會稱這個函式有Side Effect.
而什麼是改變外部狀態,舉以下幾個例子
let state = 0;
const counter = () => {
state += 1;
return state;
}
counter(); // 1
counter(); // 2
counter(); // 3
counter(); // 4
上面範例的counter 就是一個有side effect 的函式,因為我們因為呼叫了 counter 時也改變外部的狀態。當一個有程式四處散落著 side effect 的函式時,那程式變得非常不可控。
又或是在函式內進行console.log
const doWhat = (x) => {
console.log(`hahaha, ${x}`)
return x;
}
doWhat()
這個範例則是我們在呼叫 doWhat
時,同時也在 console
寫入了一些資訊,這也是一個有 Side Effect 的函式。
而會導致Side Effect 的操作,筆者列了幾項,
Wait..., 讀者們可能心想,在現實開發中,不使用上述這些會產生 Side Effect 的功能,還能進行開發嗎? 對,基本上是開發不了任何東西的。所以FP 該不會等於 no code!
不不不
FP 不是不能有Side Effect,而是將這些Side Effect用一些方式包覆起來,將 Effectful program 跟 Pure function 有個明確的界線進行劃分。在之後章節我們也會提到FP是如何解決。
為什麼純函數這個概念這麼重要? 因為它直接確保 referential transparency 的概念!
首先我們先來看看 referential transparency 的定義
An expression is called referentially transparent if it can be replaced with its corresponding value (and vice-versa) without changing the program's behavior. - wiki
舉例來說,
const x = square(2);
上述提到的範例square ,因為它是有著 referential transparency 的特性,所以我們可以將直接用4 取代
const x = 4;
此概念雖然簡單,筆者們看了這個範例或許心想 So What?
但 referential transparency 確保了
就如同剛剛上述提到的特性,當程式是由眾多純函數組成時,不用擔心我們影響外部的任何狀態,也可以確保程式執行過程中,不會產生非預期地結果。
純函式也確保了我們在測試時,不用去建造一個真實的環境去測試整個 program, 只需要給定 input 並預期其結果是正確的就好。
由於純函式的關係,確保了每個輸入對應唯一的輸出值,這就代表我們可以進行快取,舉最知名的 fibonacci number cache 過後的 fib函式與原本的 fib 的差別就是可以減少重複計算。
const memo = f => {
const cache = {};
const memoized = n => {
if(!cache[n]) {
cache[n] = f(n);
}
return cache[n]
}
return memoized;
}
const fib = n => {
if(n === 0) return 0;
if(n === 1) return 1;
return fib(n-2) + fib(n-1)
}
const memoFib = memo(fib)
memoFib(10) // 55
memoFib(10) // cache 後的 55
儘管這些概念都是基本的概念,但其概念卻是貫穿著函數式編程,明天將介紹 Currying vs. Partial Application.