點燈坊

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

將 Imperative 重構成 Functional

Sam Xiao's Avatar 2020-04-08

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 典型作法。

chain000

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() 取代。

chain001

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()

chain002

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()

chain003

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。

可將 urlgetUrl() 取代,如此 f() 只有一行而已。

由於 FP 都是 pure function 有 referential transparency 特性,可放心將 variable 以 function 取代

chain004

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。

chain005

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 則重構,否則到第五步即可

chain005

Conclusion

  • 本文展示了從 imperative 重構成 FP 的常用 SOP:
    1. 以 function 取代 operator
    2. pipe()compose() 組合 function
    3. 將原 function 內的 function 重構到 funciton 外
    4. 以 function 取代 variable
    5. 將 function 也 point-free
    6. 以其他 function 取代特定 pattern

Reference

Andy Van Slaars, Refactor to Point Free Functions with Ramda using compose and converge