Functional Programming 要求 Data 與 Function 分離,其中 Data 要求的是 Immutable,而 Function 則是 Pure Function。這是 FP 的兩大基石,所有其他的特性都是由這兩個基本原則展開。
Version
macOS Catalina 10.15.2
VS Code 1.40.2
Quokka 1.0.259
ECMAScript 2015
Pure Function
- 與數學 function
f(x)
一致 - Ouput 僅與 argument 有關
- 不會產生 side effect
與 pure function 相對的,就是 impure function:
- 傳統 function
- 除了 argument 外,尚有其他因素影響 output (class field、exception、I/O)
- 會產生 side effect
我們可發現 pure function 與 impure function 是相對的,其他定義都很容易理解,唯獨 side effect 需要另外解釋。
Side Effect
- Global state
- Input argument
- Throw exception
- I/O operation
Global State
class Counter {
#count = 0
constructor(count) {
this.#count = count
}
get count() {
return this.#count
}
addOne() {
this.#count++
return this;
}
}
let counter = new Counter(0)
counter.addOne().addOne().count // ?
凡在 function 以外的 scope,就算是 global state,如 OOP 的 field,也被視為 global state,修改 global state 被視 side effect。
OOP 強調 data 與 function 合一,所以會將 count
視為 field,封裝在 Counter
class 內。
addOne()
被視為有 side effect,因為其修改了 method 外部的 count
field,屬於 global state。
class Counter {
#count = 0
constructor(count) {
this.#count = count
}
get count() {
return this.#count
}
}
let addOne = ({ count }) => new Counter(count + 1)
let counter = new Counter(0)
let counter_ = addOne(counter)
addOne(counter_).count // ?
FP 強調 data 與 function 分家,因此 data 屬於 Counter
class,而 function 屬於 addOne()
。
addOne()
沒有 side effect,因為根據 argument 得到目前的 object,計算後回傳新的 object,完全與 function 外部無關,所以是 pure function。
這種寫法稱為 value object pattern,將 object 視為 immutable,是 FP + OOP 整合的 best practice
import { pipe } from 'ramda'
class Counter
{
#count = 0
constructor(count) {
this.#count = count
}
get count() {
return this.#count
}
}
let addOne = ({ count }) => new Counter(count + 1)
let fn = pipe(
addOne,
addOne
)
fn(new Counter(0)).count // ?
18 行
let fn = pipe(
addOne,
addOne
)
fn(new Counter(0)).count // ?
由於 addOne()
是 pure function,因此也可將兩次 addOne()
組合成 fn()
後再一起執行。
Input Argument
import { pair, reduce, filter, converge } from 'ramda'
let data = [
{ title: 'FP in JavaScript', price: 100 , quantity: 10 },
{ title: 'RxJS in Action', price: 200, quantity: 0 },
{ title: 'Speaking JavaScript', price: 300, quantity: 20 }
]
let computeTotal = (arr, deleted) => {
let result = 0
for(let x of arr) {
if (x.quantity === 0)
deleted.push(x)
else
result += x.price * x.quantity
}
return result
}
let deleted = []
computeTotal(data, deleted) // ?
deleted // ?
修改 function 的 argument,而造成 function 外界的 data 被修改,也視為 side effect。
computeTotal
被視為有 side effect,因為直接修改了 deleted
argument,造成 function 外部 data 改變。
import { pair, reduce, filter, converge } from 'ramda'
let data = [
{ title: 'FP in JavaScript', price: 100 , quantity: 10 },
{ title: 'RxJS in Action', price: 200, quantity: 0 },
{ title: 'Speaking JavaScript', price: 300, quantity: 20 }
]
let computeTotal = converge(
pair, [
reduce((a, x) => a + x.price * x.quantity)(0),
filter(({ quantity}) => quantity === 0)
]
)
computeTotal(data) // ?
由於 computedTotal()
主要處理兩件任務:計算 total()
與回傳被刪除 data,因此改回傳 tuple,如此 function 外部 data 則不受影響,因此沒有 side effect。
Throw Exception
因為以下兩個原因,throw exception 也被視為 side effect:
- Exception 屬於 function 外部 scope,也算 global state
- Throw exception 後,其他 fuction 必須去
try catch
處理,由於不是來自於 function argument,會使得其他 function 不是 pure function
FP 可改用 Maybe 取代 exception
I/O Operation
在現實世界中,不可能所有 function 都在計算或者 mapping,如
- 呼叫 API
- 寫入資料庫、寫到 console
- 讀出系統時間
這些都屬於無法避免的 side effect,但我們可將 side effect 集中在 pure function 的前後
,而不是在 function 內隨意的 side effect,如此將降低程式碼的複雜性,也較易維護。
Conclusion
- Pure function 與 side effect 為 FP 的起手式,看似基本但實務上並不容易實現,常一不小心就寫出 impure function
- I/O 是無法避免的 side effect,但 FP 能將 side effect 集中在 pure function 前後,不要散佈到各處,降低其所造成傷害
Reference
Enrico Buonanno, Functional Programming in C#