點燈坊

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

使用 ifElse() 在 Function Pipeline 中取代 if else

Sam Xiao's Avatar 2020-04-11

寫程式免不了要使用 if else 判斷,但 if else 是 Imperative 產物, Ramda 則提供 ifElse() 讓我們在 Function Pipeline 中使用。

Version

macOS Catalina 10.15.4
VS Code 1.44.0
Quokka 1.0.285
Ramda 0.27.0

Imperative

let data = [
  { title: 'FP in JavaScript', price: 100, category: 'FP' },
  { title: 'Rx in Action', price: 200, category: 'FRP' },
  { title: 'Speaking JavaScript', price: 300, category: 'JS'}
]

let f = a => {
  let result = []

  for (let x of a) {
    x_ = Object.assign({}, x)
    
    if (x.category === 'FP')
      x_.price = x.price * 0.8
    else
      x_.price = x.price * 0.9
    
    result.push(x_)
  }

  return result
}

f(data) // ?

第 1 行

let data = [
  { title: 'FP in JavaScript', price: 100, category: 'FP' },
  { title: 'Rx in Action', price: 200, category: 'FRP' },
  { title: 'Speaking JavaScript', price: 300, category: 'JS'}
]

categoryFP,其 price 將打 8 折,其餘打 9 折。

第 7 行

let f = discount1 => discount2 => arr => {
  let result = []

  for (let x of arr) {
    x_ = Object.assign({}, x)
    
    if (x.category === 'FP')
      x_.price = x.price * 0.8
    else
      x_.price = x.price * 0.9
    
    result.push(x_)
  }

  return result
}

Imperative 會先建立 result empty array,使用 for loop 對 data 一筆一筆處理,然後使用 Object.assign() clone object,若 category property 為 FP,則 price 打 8 折,其餘打 9 折,最後 push 進 result array 回傳。

ifelse000

reduce()

let data = [
  { title: 'FP in JavaScript', price: 100, category: 'FP' },
  { title: 'Rx in Action', price: 200, category: 'FRP' },
  { title: 'Speaking JavaScript', price: 300, category: 'JS'}
]

let f = a => a.reduce((a, x) => (
  (x.category === 'FP') ?
  [...a, {...x, price: x.price * 0.8 }] :
  [...a, {...x, price: x.price * 0.9 }]
), [])

f(data) // ?

只要能使用 for loop,就能使用 reduce() 改寫。

ifelse001

map()

let data = [
  { title: 'FP in JavaScript', price: 100, category: 'FP' },
  { title: 'Rx in Action', price: 200, category: 'FRP' },
  { title: 'Speaking JavaScript', price: 300, category: 'JS'}
]

let f = a => a.map(x => {
  if (x.category === 'FP')
    return {...x, price: x.price * 0.8 }
  else
    return {...x, price: x.price * 0.9 }
})

f(data) // ?

ECMAScript 比較好的方法是改用 map(),並在其 callback 中使用 if else 判斷 category 是否為 FP

ifelse002

Ramda

import { map } from 'ramda'

let data = [
  { title: 'FP in JavaScript', price: 100, category: 'FP' },
  { title: 'Rx in Action', price: 200, category: 'FRP' },
  { title: 'Speaking JavaScript', price: 300, category: 'JS'}
]

let f = map(x => {
  if (x.category === 'FP')
    return {...x, price: x.price * 0.8 }
  else
    return {...x, price: x.price * 0.9 }
})

f(data) // ?

也可使用 Ramda 的 map() 取代,可使 f() 能 point-free。

ifelse003

ifElse()

import { map, ifElse, propEq, assoc, identity, chain, prop, multiply, useWith } from 'ramda'

let data = [
  { title: 'FP in JavaScript', price: 100, category: 'FP' },
  { title: 'Rx in Action', price: 200, category: 'FRP' },
  { title: 'Speaking JavaScript', price: 300, category: 'JS'}
]

let applyDiscount = useWith(
  multiply, [identity, prop('price')]
)

let f = map(ifElse(
  propEq('category')('FP'),
  chain(assoc('price'), applyDiscount(0.8)),
  chain(assoc('price'), applyDiscount(0.9))
))

f(data) // ?

Ramda 正規方式是使用 ifElse() 取代 if else

ifElse()
(*... -> boolean) -> (*...-> *) -> (*...-> *) -> (*... -> *)
根據條件回傳不同 function

(*... -> boolean):任意 argument 的 function,只要回傳 boolean 即可
(*...-> *):任意 argument 的 function,回傳也不拘
(*...-> *):任意 argument 的 function,回傳也不拘
(*...-> *):若為 true 則回傳第二個 function,若為 false 則傳回第三個function

ifelse004

Lens

import { map, ifElse, propEq, identity, lensProp, over, multiply } from 'ramda'

let data = [
  { title: 'FP in JavaScript', price: 100, category: 'FP' },
  { title: 'Rx in Action', price: 200, category: 'FRP' },
  { title: 'Speaking JavaScript', price: 300, category: 'JS'}
]

let priceLens = lensProp('price')

let f = map(ifElse(
  propEq('category')('FP'),
  over(priceLens, multiply(0.8)),
  over(priceLens, multiply(0.9))
))

f(data) // ?

若要修改 object,透過 lens 也是不錯方式。

第 9 行

let priceLens = lensProp('price')

先使用 lensProp()price property 建立 lens。

13 行

over(priceLens, multiply(0.8)),

直接透過 over 修改 price property,不必搭配 assoc()chain()prop()

ifelse005

Conclusion

  • ifElse() 讓我們在 pipeline 也可以使用 if else 判斷,卻不會中斷 pipeline
  • 若回傳為固定值,可使用 always() 產生 function
  • 若回傳為相同值,可使用 identity() 產生 function
  • 若要修改 object,改用 lens 是很不錯方式,可避免組合 prop()assoc() 的尷尬,直接使用 over() 即可

Reference

Ramda, ifElse()
Ramda, map()
Ramda, lensProp()
Ramda, identity()