在 Martin Fowler 的 Refactoring 一書,並沒有將 Extract Higher-order Function 列為標準技法,事實上這也是實務上天天都在使用的重構方式。
Version
macOS Catalina 10.15.2
VS Code 1.41.1
Quokka 1.0.274
ECMAScript 2015
Definition
Higher-order Function
以 function 作為 argument 或以 function 為 return 值
ECMAScript 在 Array.prototype
下的 map()
、filter()
與 reduce()
… 等是典型以 function 為 argument 的 higher-order function。
Ramda 的 pipe()
、compose()
、curry()
與 Node 的 Util.promisify()
… 等則是典型以回傳 function 的 higher-order function。
Application
Higher-order function 在實務上可歸納出以下 4 種 pattern:
Inversion of Control
Adapter function
Factory function
Avoid duplication
Inversion of Control
let data = [
{ title: 'FP in JavaScript', price: 100 },
{ title: 'RxJS in Action', price: 200 },
{ title: 'Speaking JavaScript', price: 300 }
]
let filter = fn => arr => {
let result = []
for(let x of arr) {
if (fn(x))
result.push(x)
}
return result
}
filter(x => x.price === 100)(data) // ?
以 filter()
為例,低階模組 filter()
決定了整個 控制流程
,包含 for
loop 與 if
, 高階模組只決定 predicate 的 實作部分
,這就是 inversion of control。
Inversion of Control
原本由高階模組決定控制流程
,改成由低階模組
決定控制流程
,高階模組只決定實作部分
可將
控制流程
寫成 library,實現 separation of concerns:低階模組關注於控制流程
,而高階模組專心於實作部分
Higher-order function 最常使用的場景就是為了實現 inversion of control。
IoC 與 DIP (Dependency Inversion Principle 依賴反轉原則) 並不一樣,IoC 強調的是
控制流程
的反轉,而 DIP 強調的是藉由 interface 達到依賴
的反轉
Adapter Function
let divide = (x, y) => x / y
divide(10, 2) // ?
原本 divide()
的 被除數
是 x
,除數
是 y
。
因為需求改變,被除數
改成 y
,而 除數
改成 x
,也就是 signature 改變造成 argument 對調。
當然可以直接修改 code,基於 開放封閉原則
,且這也是常見的需求,可將此功能 一般化
以 adapter function 處理。
Adapter Function
Higher-order function 目的在於改變 function 的 signature
let flip = fn => (y, x) => fn(x, y)
let divide = (x, y) => x / y
flip(divide)(2, 10) // ?
第 3 行
let flip = fn => (y, x) => fn(x, y)
flip()
回傳一個新 function,其 argument 由原本的 (x, y)
改成 (y, x)
。
在 OOP 中,若 interface 不同,我們會使用 adapter pattern,將 interface 加以轉換;在 FP 中,signature 就是 interface,若 signature 不同,我們可使用 higher-order function 加以轉換,也稱為 adapter function
Factory Function
import { filter } from 'ramda'
let data = [1, 2, 3, 4, 5, 6]
let odd = filter(x => x % 2 === 0)
let tripple = filter(x => x % 3 === 0)
odd(data) // ?
tripple(data) // ?
若想找出 2
的倍數與 3
的倍數的所有 element,我們會使用 filter()
,並在 predicate 以 % 2
與 % 3
處理,
若我們想讓功能更 一般化
,能找出 n
的倍數所有 element。
import { filter } from 'ramda'
let data = [1, 2, 3, 4, 5, 6]
let mod = n => x => x % n === 0
let odd = filter(mod(2))
let tripple = filter(mod(3))
odd(data) // ?
tripple(data) // ?
第 5 行
let mod = n => x => x % n === 0
mod()
為更一般化的 factory function ,可以產生 % n
的 predicate,且可讀性也更高。
Factory Function
Higher-order function 目的就是建立新 function
第 7 行
let odd = filter(mod(2))
let tripple = filter(mod(3))
filter()
組合 mod()
可產生任何倍數 function。
import { filter, compose } from 'ramda'
let data = [1, 2, 3, 4, 5, 6]
let mod = n => x => x % n === 0
let odd = compose(filter, mod)(2)
let tripple = compose(filter, mod)(3)
odd(data) // ?
tripple(data) // ?
也可使用 Ramda 的 compose()
將 mod()
與 filter()
加以組合。
Avoid Duplication
let data = [
{ title: 'FP in JavaScript', price: 100 },
{ title: 'RxJS in Action', price: 200 },
{ title: 'Speaking JavaScript', price: 300 }
]
let anyPrice100 = arr => {
for (let x of arr)
if (x.price === 100) return true
return false
}
let anyTitleJavaScript = arr => {
for(let x of arr)
if (x.title === 'Speaking JavaScript') return true
return false
}
anyPrice100(data) // ?
anyTitleJavaScript(data) // ?
第 7 行
let anyPrice100 = arr => {
for (let x of arr)
if (...) return true
return false
}
我們可發現 anyPrice100()
與 anyTitleJavaScript()
非常相似,除了 if()
的判斷外,其餘都相同,因此我們想將重複部分抽成 higher-order function。
let data = [
{ title: 'FP in JavaScript', price: 100 },
{ title: 'RxJS in Action', price: 200 },
{ title: 'Speaking JavaScript', price: 300 }
]
let any = fn => arr => {
for (let x of arr)
if (fn(x)) return true
return false
}
let anyPrice100 = any(x => x.price === 100)
let anyTitleJavaScript = any(x => x.title === 'Speaking JavaScript')
anyPrice100(data) // ?
anyTitleJavaScript(data) // ?
第 7 行
let any = fn => arr => {
for (let x of arr)
if (fn(x)) return true
return false
}
將共同部分抽出來,不同部分以 fn
argument 傳入。
Avoid Duplication
Higher-order function 目的在抽出程式碼共用部分
14 行
let anyPrice100 = any(x => x.price === 100)
let anyTitleJavaScript = any(x => x.title === 'Speaking JavaScript')
anyPrice100()
與 anyTitleJavaScript()
改以 any()
產生,只傳入不同部分。
Conclusion
- Higher-order function 已經是目前所有程式語言都能接受觀念,儘管是 OOP,也都能夠接受 higher-order function
- 過度使用 higher-order function 反而會使得 code 過度抽象化而難以理解,記得要以
可讀性
為前提適當地使用
Reference
Enrico Buonanno, Functional Programming in C#