點燈坊

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

使用 unless() 在 Function Pipeline 中取代 if not

Sam Xiao's Avatar 2020-04-12

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

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'}
]

category 不為 FP,其 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
}

f(data) // ?

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

unless000

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() 改寫。

unless001

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

unless002

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。

unless003

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'),
  identity,
  chain(assoc('price'), applyDiscount(0.8))
))

f(data) // ?

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

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

unless004

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'),
  identity,
  over(priceLens, multiply(0.8)),
))

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

unless005

unless()

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

f(data) // ?

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

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

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

(a -> a)false 所執行 function

a:傳入 data

a:回傳 data

unless006

Conclusion

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

Reference

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