iT邦幫忙

2018 iT 邦幫忙鐵人賽
DAY 5
3

本章重點

  • Pure Function 的定義為 相同的輸入,必定會得到相同的輸出 ,沒有任何副作用。
  • 因為有固定輸入輸出,讓 Pure Funciton 能安全、可移植、可測試、可緩存。
  • Pure Funciton 與 數學上定義的函數相同 ,這讓數學上的觀念能在程式中實踐,這衍生出了 CurryCompose

在開始之前

這兩篇有大量的 JS 語法講解,如果你覺得看不太懂程式碼,可以先看看這兩篇。

純粹是唯一目的

Pure Function 的目的就像是 Immutable Data ,竭盡所能的避開副作用。 Pure Function 只在乎輸入與輸出,相同的輸入,必定拿到相同的輸出,不能依據外部狀態,若這週可以執行,下禮拜就爆炸,也不是 Pure Function 。
像是 全域變數外部環境交互作用 ,有可能會影響輸出結果,這些都被認為是副作用,舉例來說像是:

  • 改變外部變數
  • 存取系統狀態
  • 讀 / 寫檔案
  • CRUD 資料庫
  • 取得使用者輸入 / 輸出至畫面
  • Random

基本四則運算,這些都是 Pure Function 。

const add = (x, y) => x + y
const subtract = (x, y) => x - y
const multiply = (x, y) => x * y
const divide = (x, y) => x / y
const mod = (x, y) => x % y

這個 Object 怪怪的 中的 TodoList 全都不是 Pure Function ,另一個很明顯的例子,像是 Array.sliceArray.splice ,其中 Immutable Data 能夠讓我們更確定這 Function 是安全無疑的。

const array1 = [1, 2, 3, 4, 5]
Object.freeze(array1)

const array2 = array1.slice(0,3)
console.log(
    array2 // [1, 2, 3]
)

const array3 = array1.splice(0,3)
// Uncaught TypeError: Cannot add/remove sealed array elements
//   at Array.splice (<anonymous>)

依據全域變數的這種也不是 Pure Function 。

let minimum = 18
const isBigger = x => x > minimum

除了輸入值以外, minimum 有可能影響輸出結果,這種也不是 Pure Function 。如果 minimum 是藉由參數傳進來,那 isBigger 就是一個 Pure Function 。

const isBigger = minimum =>
    x => x > minimum

const isBiggerThan18 = isBigger(18)

純粹的好處

  • 容易閱讀。
  • 安全、可移植。
  • 輕易 Unit Test,可以並行測試(你不需要一個狀態,然後依序執行)。
  • 因為輸入輸出是固定的,可以緩存內容,避免重複運算。

來個 快取乘法 簡單實作!

const multiply = (x, y) => x * y
const memoize = (func, keyGen = JSON.stringify) => {
	let cache = {} // cache 儲存的地方
	return (...args) => {
        // 我們需要一個 key 來區分 cache
        // 像是 (2, 2) => 4
        // 預設的 keyGen 為 JSON.stringify
        
        // keyGen(args)
        //     -> keyGen([2, 2])
		//     -> JSON.stringify([2, 2])
        //     -> "[2,2]" 
        // cache["[2,2]"] = func.apply(null, args)
        
        const key = keyGen(args)
		if (!(key in cache)) {
			cache[key] = func.apply(null, args)
		}
		return cache[key]
	}
}

const mMultiply = memoize(multiply)

這不是個健康的 memoize function ,因為在 JSON.stringify 會造成不必要的負擔(但適用任何 case ),正確使用是要傳入一個自訂的 keyGen ,但它已經成功達成目的了。

函數式編程的全部

Pure function 與一般的 Function 不同,而不同的點就是他們是一種對應關係,只要輸入,就有對應的輸出,不受其他因素控制

這不就是 國二數學裡的函數 嗎?

在國中二年級的數學課,我們就學到了函數,當時老師說了函數是一種對應關係,只要輸入,就有對應的輸出,不受其他因素控制,那時還說了只能一對一、多對一,不能一對多、一對無,我全部都想起來了,原來我早就學過了阿,謝謝你,謝啟凰老師 [1]

假如有一個函數, y = x + b ,如果固定 b 為 1 ,那就得到了一個全新的函數 y = x + 1 ,這也正是我剛在 isBiggerThan18 做的事。

const add = b => 
    x => x + b
const increment = add(1)

console.log(
    increment(2) // 3
)

假如有兩個函數 fg

  1. 執行 g ,得到 g(x) 。
  2. 把結果傳給 g(x) 傳給 f ,得到 f(g(x)) ,又記做 (f o g)(x)
  3. 把 f 與 g 合成在一起,成為了一個全新的函數 f o g
const compose = (f, g) => 
    (...args) => f(g.apply(null, args))

const add2 = x => x + 2
const multiply3 = x => 3 * x
const foo = compose(add2, multiply3)

console.log(
    foo(2) // 8
)

這就是 Functional Programming 最核心的概念,我們可以使用函數去產生函數,然後把函數組合起來!而且沒有任何副作用!

天呀!這真的是個了不起的構想!

而這兩個概念分別稱之為 CurryCompose ,在 Scala day 9 (Currying) 中也有提到,這兩個概念的重要性,足以分開寫兩篇文章。

我們先來個簡單總結:

  • Curry
    • 能給參數,讓 function 轉換為一個新的 function 。
  • Compose
    • 能把多的參數組合在一起,產生一個新的 function 。

如何面對 Impure Function

當然,現實世界不可能只有 Pure function ,總有需要使用者輸入的時候吧,有一件事是非常確定的:有可能有錯誤發生,不過這不是件另人沮喪的事,至少我們有大量面對這種狀況的經驗。

在 FP 中需要更多工具來處理, MayBeEither ... (別擔心我之後會詳細的介紹它們),現在我們能先撰寫 Pure 的部份,並與 Impure 的部份一同使用,現在我們的代碼已經比以往清楚很多了。

後記

我開始理解為什麼很多人都斷在第九篇左右了,可以跟我說一聲加油嗎?

參考資料


上一篇
最詳細 Immutable Data 入門,看完秒懂
下一篇
Higher order Function = { Map, Filter } 與 Recursion
系列文
30天快樂學習 Functional Programming14
0
weichun0911
iT邦見習生 0 級 ‧ 2017-12-18 13:32:09

加油! 一定要完賽啊>_<

ifhange iT邦新手 5 級‧ 2017-12-19 02:14:50 檢舉

加油+1哈哈

阿志 iT邦新手 5 級‧ 2017-12-22 22:43:58 檢舉

謝謝你 >_<

0
Xuan
iT邦新手 5 級 ‧ 2017-12-20 17:46:26

加油加油

阿志 iT邦新手 5 級‧ 2017-12-22 22:44:41 檢舉

謝謝你的支持~

0
biggy2003211
iT邦新手 5 級 ‧ 2017-12-20 23:23:52

加油!!!!!!!

阿志 iT邦新手 5 級‧ 2017-12-22 22:44:11 檢舉

謝謝你的支持!

0
nihilitypeo
iT邦新手 5 級 ‧ 2017-12-23 18:06:37

您好,想請問一下這一行:
cache[key] = func.apply(null, args)
為什麼要用apply?this的指向並沒有需要被改變,
這裡可以用func(args)就可以了嗎?
感謝!

阿志 iT邦新手 5 級‧ 2017-12-23 18:26:02 檢舉

您好,
這邊的 args 是一個陣列(有附帶 s 的變數名稱通常是一個陣列)

const args = [1, 2, 3]
func.apply(null,args) // func(1, 2, 3)
func(args) // func([1, 2, 3])

所以兩者不一樣,
但是可以用 func(...args)

感謝你的發問

喔喔!原來是參數的形式不同!感謝!

我要留言

立即登入留言