functional light programming 筆記
這邊是基於 front end master 上的 functional light programming[^1] 學習筆記。雖然一直都有使用 rxjs 但對於 functional programming (FP) 到底是什麼? 可以怎麼擴大使用不是很了解,所以想補一些 FP 基礎跟實際寫一些應用來體驗一下 FP。
Before Functional Programming
這一段落建議可以先略過,等到後面有一些 JavaScript 用法不是很了解為什麼可行的時候再回來閱讀。
JavaScript 的 Function 有幾個特性:
First Class Function
Function 是一等公民:亦即可以被當成變數一樣來處理,可以當作參數傳、也可以存進一個變數。
function fn(a) {
return f(b) {
return a + b
}
}
console.log(fn(a)(b))
Function 的參數其實是一個 arr obj,透過解構的手法可以變成一個個變數。
Type Signature
- parameter vs argument: parameter 是指原本 function 定義上的參數, argument 是 function call 時實際傳入的參數。
- type signature: 簡言之就是 function 需要怎麼樣的 parameter 又會回傳什麼,也就是 function 的形狀。
之所以 type signature 重要是因為這關係到 function 跟 function 之間好不好組合? 通常越多 parameter 則越難搭配。
這邊可以透過 Higher Order Function (HOF) 來處理 type signature 的問題。
- HOF: 可以傳入一個以上的 function 或回傳一個以上的 function 的 function,簡言之 input 或 output 裡面有 function 的就是 HOF。
function unary(fn) {
return function one(arg) {
return fn(arg)
}
}
function binary(fn) {
return function two(arg1, arg2) {
return fn(arg1, arg2)
}
}
function f(...args) {
return args;
}
let g = unary(f)
let h = binary(f)
g(1,2,3,4)
h(1,2,3,4)
Flip adapter:你也可以交換 argument 的順序,有的時候如果發現 function 沒辦法被拼起來,可以多透過 adapter 技巧來做轉換。
function reverseArgs(fn) {
return function reversed(...arg) {
return fn(...arg.reverse())
}
}
function f(...args) {
return args;
}
let g = reverseArgs(f)
g(1,2,3,4)
- Spread Arguments:你也可以把 args 從一轉多,透過使用 Array 的解構。
function spreadArgs(fn) {
return function spread(arg) {
return fn(...args)
}
}
function f(x, y, z, w) {
return x + y + z + w;
}
let g = spreadArgs(f)
g([1,2,3,4])
Functional Programming
function vs procedure: (pure) function 指得是 input 與 output 之間的關係,procedure 則是指這以外我們平常稱之為 functions 的程序,通常帶有 side effect,會造成 input 與 output 之外的影響。
Side Effect
在 function 之外造成的影響,導致每一次 function 執行可能會有不同的結果,使得 input 與 output 的關係不一致 e.g. IO, DB storage, Networks, DOM, timestamps, random,我們不可能消除 side effect,但我們要最小化 side effect 的影響。
side effect 最大的問題在於如果今天出了錯必須瀏覽所有程式碼找出在 function 之外的影響才有辦法解決問題。
想像自己今天只看到了局部的程式碼,你有沒有足夠的信任度可以相信這個程式碼本身就是不會影響到其他環境,如果可以這就是一個合格的 functional programming
例如:如果今天只有這一段程式碼,會覺得是 pure function 嗎?
function getId(obj) {
return obj.id;
}
可能有點懷疑就代表這不是一個值得信任的 function,所以結論而言 function purity 是一種信心指數,並非二分法的結論,functional programming 在做的是盡可能提高 function purity 的信心指數。
Impurity
然而真實世界的程式碼不可能沒有 side effect,所以儘可能最小化、分離邊緣化讓 side effect 的區塊變得明顯是重要的,有幾種做法可以做到:
Extracting Impurity
面對 side effect 的處理方式最好的方法是將 purity 與 impurity 的部分分離,並且讓 side effect 變的更清晰。
// pure & impure 沒有分離
function addComment(userId, comment) {
document.querySelector(...).innerHTML = `
<div data-id="userId" id="${uniqueId()}">${comment}</div>
`
}
addComment(10, 'hello');
// pure & impure 分離
function newComment(userId, comment, commentId) {
return `<div data-id="userId" id="commentId">${comment}</div>`
}
const commentId = uniqueId();
const ele = newComment(10, 'hello', commentId);
document.querySelector(...).innerHTML = ele;
Containing Impurity
另外一種方式是不要讓 impurity 外溢,限制在一個 function 之內。
array case
const someAPI = {
threshold: 13,
isBelowThreshold(x) {
return x < someAPI.threshold
}
}
const arr = [];
// splice 會造成 arr impure
function insertSortDesc(nums, v) {
someAPI.threshold = v;
let idx = numbers.findIndex(someAPI.isBelowThreshold)
if(idx === -1) {
idx = numbers.length
}
numbers.splice(idx, 0, v)
}
- wrapper: 可以直接 function 包含 function
function sortNumber(noms, v) {
let numbers = noms.slice();
insertSortDesc(v)
return numbers;
function insertSortDesc(nums, v) {
someAPI.threshold = v;
let idx = numbers.findIndex(someAPI.isBelowThreshold)
if(idx === -1) {
idx = numbers.length
}
numbers.splice(idx, 0, v)
}
}
- adapter: 透過一個事先保存事後還原的做法,開一個轉換 function 來處理 side effect。
function insertSortDesc(nums, v) {
someAPI.threshold = v;
let idx = numbers.findIndex(someAPI.isBelowThreshold)
if(idx === -1) {
idx = numbers.length
}
numbers.splice(idx, 0, v)
}
function sortNumber(noms, v) {
const [originNum, originThreshold] = [numbers, someAPI.threshold]
let numbers = nums.slice();
insertSortDesc(v)
nums = numbers;
[numbers, someAPI.threshold] = [originNum, originThreshold];
return numbers;
}
Point-Free
嚴格來說 Point Free Function 的定義是,不需要定義 input 的 function,裡面集合了各種使用其他 function 來處理這個問題的小技巧。
他會創造出一種程式風格,類似於一般所說 declarative 的風格,寫你想要什麼(What)而非讓程式做什麼(How),要做到這個風格必須要讓 function 之間的關係更加顯著,以下方為例:
function isOdd(v) {
return v % 2 === 1
}
function isEven(v) {
return !isOdd(v)
}
isEven(4)
比起直接將 isEven 定義為 v % 2 === 0
,因為這樣就可以讓 isEvent 為 isOdd 的相反。
但更好的寫法可以善用 HOF,把 Not 的關係揭露出來:
function not(fn) {
return function negated(...args) {
return !fn(...args)
}
}
function isOdd(v) {
return v % 2 === 1
}
const isEven = not(isOdd)
isEven(4)
這邊會需要讓 Function 做到 Equational Reasoning,讓作為參數的 function 的「形狀」與 Callback Function 的「形狀」相同。
Memoize
Functional Programming 之所以成立的一個核心,就是有 Closure 機制在記憶變數(我們使用的 function)但要小心變數上的記憶可能會讓每次回傳的值不同。
function repeater(count) {
// option 1 eager
// var str = "".padStart(count, "A")
return function allTheAs() {
return str;
// option 2 lazy
// return "".padStart(count, "A")
}
}
const A = repeater(10);
A()
A()
- Lazy: 每一次呼叫都會執行 padStart,每次呼叫都會執行一次
- Eager: 只有第一次的時候會執行 padStart,缺點如果沒有呼叫的話就白執行了,
function repeater(count) {
let str;
return function allTheAs() {
if(str === undefined) {
str = "".padStart(count, "A")
}
return str;
}
}
使用了 closure 讓 str 可以被記憶,疑似有 impurity 的可能性存在,但實際執行上的確 input 跟 output 永遠一致,這樣有比較好的效能表現,不過這不是一個 functional style 的寫法
Composition
前面提到了很多 function 元件,現在如果要將這些 function 組合再一起,我們會需要 Pipe, Componse 這類的 HOF 來協助。
- Pipe Left to Right
- Compose Right to Left