30 天精通 RxJS (02):Functional Programming 基本观念
Functional Programming 是 Rx 最重要的观念之一,基本上只要学会 FP 要上手 Rx 就不难了!Functional Programming 可以说是近年来的显学,各种新的函式编程语言推出之外,其他旧有的语言也都在新版中加强对 FP 的支援!
什麼是 Functional Programming ?
Functional Programming 是一种编程范式(programming paradigm),就像 Object-oriented Programming(OOP)一样,就是一种写程式的方法论,这些方法论告诉我们如何思考及解决问题。
简单说 Functional Programming 核心思想就是做运算处理,并用 function 来思考问题,例如像以下的算数运算式:
(5 + 6) - 1 * 3
我們可以寫成
const add = (a, b) => a + b const mul = (a, b) => a * b const sub = (a, b) => a - b sub(add(5, 6), mul(1, 3))
我们把每个运算包成一个个不同的 function,并用这些 function 组合出我们要的结果,这就是最简单的 Functional Programming。
Functional Programming 基本要件
跟 OOP 一样不是所有的语言都支持 FP,要能够支持 FP 的语言至少需要符合函式為一等公民的特性。
函式為一等公民 (First Class)
一等公民就是指跟其他资料型别具有同等地位,也就是说函式能够被赋值给变数,函式也能够被当作参数传入另一个函式,也可当作一个函式的回传值
函式能夠被賦值給變數
var hello = function() {}
函式能被當作參數傳入
fetch('www.google.com').then(function(response) {}) // 匿名 function 被傳入 then()
函式能被當作回傳值
var a = function(a) { return function(b) { return a + b; }; // 可以回傳一個 function }
Functional Programming 重要特性
Expression, no Statement
Functional Programming 都是 表达式 (Expression) 不会是 陈述式(Statement)。 基本区分表达式与陈述式:
表達式 是一個運算過程,一定會有返回值,例如執行一個 function
add(1,2)
陳述式 則是表現某個行為,例如一個 賦值給一個變數
a = 1;
有時候表達式也可能同時是合法的陳述式,這裡只講基本的判斷方法。如果想更深入了解其中的差異,可以看這篇文章 Expressions versus statements in JavaScript
由於 Functional Programming 最早就是為了做運算處理不管 I/O,而 Statement 通常都屬於對系統 I/O 的操作,所以 FP 很自然的不會是 Statement。
當然在實務中不可能完全沒有 I/O 的操作,Functional Programming 只要求對 I/O 操作限制到最小,不要有不必要的 I/O 行為,盡量保持運算過程的單純。
Pure Function
Pure function 是指 一個 function 給予相同的參數,永遠會回傳相同的返回值,並且沒有任何顯著的副作用(Side Effect)
舉個例子:
var arr = [1, 2, 3, 4, 5]; arr.slice(0, 3); // [1, 2, 3] arr.slice(0, 3); // [1, 2, 3] arr.slice(0, 3); // [1, 2, 3]
這裡可以看到 slice 不管執行幾次,返回值都是相同的,並且除了返回一個值(value)之外並沒有做任何事,所以 slice
就是一個 pure function。
var arr = [1, 2, 3, 4, 5]; arr.splice(0, 3); // [1, 2, 3] arr.splice(0, 3); // [4, 5] arr.slice(0, 3); // []
這裡我們換成用 splice
,因為 splice
每執行一次就會影響 arr
的值,導致每次結果都不同,這就很明顯不是一個 pure function。
Side Effect
Side Effect 是指一個 function 做了跟本身運算返回值沒有關係的事,比如說修改某個全域變數,或是修改傳入參數的值,甚至是執行 console.log
都算是 Side Effect。
Functional Programming 強調沒有 Side Effect,也就是 function 要保持純粹,只做運算並返回一個值,沒有其他額外的行為。
這裡列舉幾個前端常見的 Side Effect,但不是全部
發送 http request
在畫面印出值或是 log
獲得使用者 input
Query DOM 物件
Referential transparency
前面提到的 pure function 不管外部環境如何,只要參數相同,函式執行的返回結果必定相同。這種不依賴任何外部狀態,只依賴於傳入的參數的特性也稱為 引用透明(Referential transparency)
利用參數保存狀態
由於最近很紅的 Redux 使我能很好的舉例,讓大家了解什麼是用參數保存狀態。了解 Redux 的開發者應該會知 Redux 的狀態是由各個 reducer 所組成的,而每個 reducer 的狀態就是保存在參數中!
function countReducer(state = 0, action) {// ...}
如果你跟 Redux 不熟可以看下面遞回的例子
function findIndex(arr, predicate, start = 0) { if (0 <= start && start < arr.length) { if (predicate(arr[start])) { return start; } return findIndex(arr, predicate, start+1); }} findIndex(['a', 'b'], x => x === 'b'); // 找陣列中 'b' 的 index
這裡我們寫了一個 findIndex 用來找陣列中的元素位置,我們在 findIndex
中故意多塞了一個參數用來保存當前找到第幾個 index 的狀態,這就是利用參數保存狀態!
這邊用到了遞回,遞回會不斷的呼叫自己,製造多層 stack frame,會導致運算速度較慢,而這通常需要靠編譯器做優化!
那 JS 有沒有做遞回優化呢? 恭喜大家,ES6 提供了 尾呼優化(tail call optimization),讓我們有一些手法可以讓遞回更有效率!
Functional Programming 優勢
可讀性高
当我们透过一系列的函式封装资料的操作过程,程式码能变得非常的简洁且可读性极高,例如下面的例子
[9, 4].concat([8, 7]) // 合併陣列 .sort() // 排序 .filter(x => x > 5) // 過濾出大於 5 的
可維護性高
因為 Pure function 等特性,执行结果不依赖外部状态,且不会对外部环境有任何操作,使 Functional Programming 能更好的除错及撰写单元测试。
易於併行/平行处理
Functional Programming 易於做併行/平行(Concurrency/Parallel)处理,因為我们基本上只做运算不碰 I/O,再加上没有 Side Effect 的特性,所以较不用担心 deadlock 等问题。