點燈坊

失くすものさえない今が強くなるチャンスよ

Extract Higher-order Function

Sam Xiao's Avatar 2020-01-24

在 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:低階模組關注於 控制流程,而高階模組專心於 實作部分

hof003

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

hof004

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。

hof000

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() 加以組合。

hof001

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() 產生,只傳入不同部分。

hof005

Conclusion

  • Higher-order function 已經是目前所有程式語言都能接受觀念,儘管是 OOP,也都能夠接受 higher-order function
  • 過度使用 higher-order function 反而會使得 code 過度抽象化而難以理解,記得要以 可讀性 為前提適當地使用

Reference

Enrico Buonanno, Functional Programming in C#