Imperative 的特徵是不斷使用中繼 Variable 傳來傳去,本文示範如何從 Imperative 重構成 FP,並歸納出重構 SOP。
Version
macOS Catalina 10.15.4
VS Code 1.43.2
Quokka 1.0.285
Ramda 0.27.0
Crocks 0.12.4
Wink-fp 1.20.69
Imperative
let data = {
id: 1
}
let makeUrl = id => `https://fpjs.fun/${id}.png`
let f = o => {
let url = makeUrl(o.id)
return { ...o, url }
}
f(data) // ?
第 5 行
let makeUrl = id => `https://fpjs.fun/${id}.png`
makeUrl()
使 id
變成 url
。
第 7 行
let f = o => {
let url = makeUrl(o.id)
return { ...o, url }
}
從 object 取得 id
變成 url
,再將 url
新增至 object 回傳。
透過中繼 variable 是 imperative 典型作法。
Ramda
import { pipe, assoc } from 'ramda'
import { prop, option } from 'crocks'
import { format } from 'wink-fp'
let data = {
id: 1
}
let makeUrl = format('https://fpjs.fun/{}.png')
let f = o => {
let url = makeUrl(pipe(prop('id'), option('N/A'))(o))
return assoc('url', url, o)
}
f(data) // ?
重構第一步是以 function 取代 operator。
Template string 可用 Wink-fp 的 format()
取代。
.
dot operator 可用 Crocks 的 prop()
取代,因為它回傳 maybe,所以必須組合 option()
。
新增 property 可用 Ramda 的 assoc()
取代。
Function Pipeline
import { pipe, assoc } from 'ramda'
import { prop, option } from 'crocks'
import { format } from 'wink-fp'
let data = {
id: 1
}
let makeUrl = format('https://fpjs.fun/{}.png')
let f = o => {
let url = pipe(
prop('id'),
option('N/A'),
makeUrl
)(o)
return assoc('url', url, o)
}
f(data) // ?
重構第二步是以 pipe()
或 compose()
組合 function。
事實上連 makeUrl()
也可以一起 pipe()
。
Extract Function
import { pipe, assoc } from 'ramda'
import { prop, option } from 'crocks'
import { format } from 'wink-fp'
let data = {
id: 1
}
let makeUrl = format('https://fpjs.fun/{}.png')
let getUrl = pipe(
prop('id'),
option('N/A'),
makeUrl
)
let f = o => assoc('url', getUrl(o), o)
f(data) // ?
重構第三步是將原 function 內的 function 重構到 funciton 外。
可將 pipe()
所組合 function 獨立成 getUrl()
。
Inline Variable
import { pipe, assoc } from 'ramda'
import { prop, option } from 'crocks'
import { format } from 'wink-fp'
let data = {
id: 1
}
let makeUrl = format('https://fpjs.fun/{}.png')
let getUrl = pipe(
prop('id'),
option('N/A'),
makeUrl
)
let f = o => assoc('url', getUrl(o), o)
f(data) // ?
重構第四步是以 function 取代 variable。
可將 url
以 getUrl()
取代,如此 f()
只有一行而已。
由於 FP 都是 pure function 有 referential transparency 特性,可放心將 variable 以 function 取代
converge()
import { pipe, assoc, converge, identity } from 'ramda'
import { prop, option } from 'crocks'
import { format } from 'wink-fp'
let data = {
id: 1
}
let makeUrl = format('https://fpjs.fun/{}.png')
let getUrl = pipe(
prop('id'),
option('N/A'),
makeUrl
)
let f = converge(
assoc('url'), [getUrl, identity]
)
f(data) // ?
重構第五步是將 function 也 point-free。
f()
的 o
argument 可用 converge()
加以 point-free。
chain()
import { pipe, assoc, chain } from 'ramda'
import { prop, option } from 'crocks'
import { format } from 'wink-fp'
let data = {
id: 1
}
let makeUrl = format('https://fpjs.fun/{}.png')
let getUrl = pipe(
prop('id'),
option('N/A'),
makeUrl
)
let f = chain(assoc('url'), getUrl)
f(data) // ?
重構第六步是以其他 function 取代特定 pattern。
converge()
+ identity()
pattern 剛好是 chain()
,可再進一步重構。
這個步驟是 optional,若找得到特定 pattern 則重構,否則到第五步即可
Conclusion
- 本文展示了從 imperative 重構成 FP 的常用 SOP:
- 以 function 取代 operator
- 以
pipe()
或compose()
組合 function - 將原 function 內的 function 重構到 funciton 外
- 以 function 取代 variable
- 將 function 也 point-free
- 以其他 function 取代特定 pattern
Reference
Andy Van Slaars, Refactor to Point Free Functions with Ramda using compose and converge