點燈坊

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

使用 when() 在 Function Pipeline 中取代 if

Sam Xiao's Avatar 2020-04-12

寫程式免不了要使用判斷邏輯,若只有 Predicate 為 true 才執行 Function,false 回傳原值,Ramda 提供了 when()

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

    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 折,否則維持不變。

第 7 行

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

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

    result.push(x_)
  }

  return result
}

Imperative 會先建立 result empty array,使用 for loop 對 data 一筆一筆處理,然後使用 Object.assign() clone object,若 category property 為 FP,則 price 打 8 折,最後 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]
), [])

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

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

f(data) // ?

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

ifelse003

ifElse()

import { map, ifElse, propEq, assoc, identity, chain, prop, useWith, 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 applyDiscount = useWith(
  multiply, [identity, prop('price')]
)

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

f(data) // ?

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

propEq()true 時,將使用 chain() 組合 assoc()applyDiscount() 改變 price(),否則改用 identity() 維持不變。

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)),
  identity
))

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

when()

import { map, when, 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(when(
  propEq('category', 'FP'),
  over(priceLens, multiply(0.8))
))

f(data) // ?

與其 ifElse() 搭配 identity(),Ramda 的提供了更精簡的 when(),只有當 true 時才會執行該 function,false 回傳原值。

when()
(a → Boolean) → (a → a) → a → a
當 Predicate 為 true 時執行 function,否則回傳原值

(a -> Boolean):predicate function,回傳 truefalse

(a -> a)true 所執行 function

a:傳入 data

a:回傳 data

when006

Conclusion

  • 若要修改 object,改用 lens 是很不錯方式,可避免組合 prop()assoc() 的尷尬,直接使用 over() 即可
  • 當遇到 ifElse(f, t, identity) 這種 pattern 時,可考重構成 when()

Reference

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