點燈坊

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

如何根據 ID 對應 Object 產生新欄位 ?

Sam Xiao's Avatar 2020-02-22

實務上 API 常常只回傳 ID,前端必須根據 ID 查出所對應的 String 顯示,這常見需求該如何使用 Ramda 實現呢 ?

Imperative

let data = [
  { title: 'FP in JavaScript', price: 100, categoryId: 0 },
  { title: 'Rx in Action', price: 200, categoryId: 1 },
  { title: 'Speaking JavaScript', price: 300, categoryId: 2 }
]

let categoryMap = {
  0: 'FP',
  1: 'FRP',
  2: 'JS'
}

let f = obj => arr => {
  let result = []

  for(let x of arr) {
    let x_ = Object.assign({}, x)
    x_['category'] = obj[x.categoryId]
    result.push(x_)
  }

  return result
}

console.dir(f(categoryMap)(data))

第 1 行

let data = [
  { title: 'FP in JavaScript', price: 100, categoryId: 0 },
  { title: 'Rx in Action', price: 200, categoryId: 1 },
  { title: 'Speaking JavaScript', price: 300, categoryId: 2 }
]

data 只有 categoryId

第 7 行

let categoryMap = {
  0: 'FP',
  1: 'FRP',
  2: 'JS'
}

需自行根據 categoryMap object 所對應的 value 顯示。

13 行

let f = obj => arr => {
  let result = []

  for(let x of arr) {
    let x_ = Object.assign({}, x)
    x_['category'] = obj[x.categoryId]
    result.push(x_)
  }

  return result
}

f() 需傳入 objarr,其中 obj 為查詢 id 與對應 value 的 object,arr 為原始 data。

Imperative 會先建立 result empty array,使用 for loop 對 data 一筆一筆處理,然後使用 Object.assign() clone object,對新 object 新增 category property,其 value 使用 categoryId 對照而來,最後 push 進 result array 回傳。

assoc000

ECMAScript

reduce()

let data = [
  { title: 'FP in JavaScript', price: 100, categoryId: 0 },
  { title: 'Rx in Action', price: 200, categoryId: 1 },
  { title: 'Speaking JavaScript', price: 300, categoryId: 2 }
]

let categoryMap = {
  0: 'FP',
  1: 'FRP',
  2: 'JS'
}

let f = obj => arr => arr.reduce((a, x) => [...a, {...x, category: obj[x.categoryId]}], [])

console.dir(f(categoryMap)(data))

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

assoc007

map()

let data = [
  { title: 'FP in JavaScript', price: 100, categoryId: 0 },
  { title: 'Rx in Action', price: 200, categoryId: 1 },
  { title: 'Speaking JavaScript', price: 300, categoryId: 2 }
]

let categoryMap = {
  0: 'FP',
  1: 'FRP',
  2: 'JS'
}

let f = obj => arr => arr.map(x => ({
  ...x,
  category: obj[x.categoryId]
}))

console.dir(f(categoryMap)(data))

ECMAScript 可使用其 Array.prototype.map(),並搭配 ... object spread 展開 object 建立新 object。

assoc001

Ramda

import { map } from 'ramda'

let data = [
  { title: 'FP in JavaScript', price: 100, categoryId: 0 },
  { title: 'Rx in Action', price: 200, categoryId: 1 },
  { title: 'Speaking JavaScript', price: 300, categoryId: 2 }
]

let categoryMap = {
  0: 'FP',
  1: 'FRP',
  2: 'JS'
}

let f = obj => map(x => ({
  ...x,
  category: obj[x.categoryId]
}))

console.dir(f(categoryMap)(data))

可使用 Ramda 的 map 將 arr argument point-free。

assoc002

assoc()

import { map, assoc, prop } from 'ramda'

let data = [
  { title: 'FP in JavaScript', price: 100, categoryId: 0 },
  { title: 'Rx in Action', price: 200, categoryId: 1 },
  { title: 'Speaking JavaScript', price: 300, categoryId: 2 }
]

let categoryMap = {
  0: 'FP',
  1: 'FRP',
  2: 'JS'
}

let f = obj => map(x => assoc('category')(obj[prop('categoryId')(x)])(x))

console.dir(f(categoryMap)(data))

對於新增 property,Ramda 提供了 assoc(),如此可不再使用 ... object spread 展開 object。

assoc003

chain()

import { map, assoc, prop, chain, compose, flip } from 'ramda'

let data = [
  { title: 'FP in JavaScript', price: 100, categoryId: 0 },
  { title: 'Rx in Action', price: 200, categoryId: 1 },
  { title: 'Speaking JavaScript', price: 300, categoryId: 2 }
]

let categoryMap = {
  0: 'FP',
  1: 'FRP',
  2: 'JS'
}

let f = obj => map(
  chain(assoc('category'), compose(flip(prop)(obj), prop('categoryId')))
)

console.dir(f(categoryMap)(data))

assoc() 常與其他 function 透過 chain() 組合,之後透過 compose() 組合 prop()flip() 從 object 查出 value。

assoc004

prop_()

import { map, assoc, prop, chain, compose, flip } from 'ramda'

let data = [
  { title: 'FP in JavaScript', price: 100, categoryId: 0 },
  { title: 'Rx in Action', price: 200, categoryId: 1 },
  { title: 'Speaking JavaScript', price: 300, categoryId: 2 }
]

let categoryMap = {
  0: 'FP',
  1: 'FRP',
  2: 'JS'
}

let prop_ = flip(prop)

let f = obj => map(
  chain(assoc('category'), compose(prop_(obj), prop('categoryId')))
)

console.dir(f(categoryMap)(data))

chain() 後面一堆 function 可讀性不高,由於 curried function 特性,可任意抽出 function。

可先將 flip(prop) 抽成 prop_()

assoc005

consult()

import { map, assoc, prop, chain, compose, flip } from 'ramda'

let data = [
  { title: 'FP in JavaScript', price: 100, categoryId: 0 },
  { title: 'Rx in Action', price: 200, categoryId: 1 },
  { title: 'Speaking JavaScript', price: 300, categoryId: 2 }
]

let categoryMap = {
  0: 'FP',
  1: 'FRP',
  2: 'JS'
}

let prop_ = flip(prop)

let consult = obj => compose(prop_(obj), prop('categoryId'))

let f = obj => map(
  chain(assoc('category'), consult(obj))
)

console.dir(f(categoryMap)(data))

第 7 行

let consult = obj => compose(prop_(obj), prop('categoryId'))

compose() 一般也避免放在 callback 內,建議抽成獨立 function 增加可讀性。

19 行

let f = obj => map(
  chain(assoc('category'), consult(obj))
)

如此就可一目瞭然看出由 assoc()consult() 透過 chain() 組合出 callback。

assoc006

Conclusion

  • chain() 屬於 Ramda 較高難度應用,但因為 assoc() 常與 chain() 搭配,只要看到 assoc() 就要想到 chain()
  • 凡使用 flip()compose() 都建議抽成獨立 function 增加可讀性

Reference

Ramda, assoc()
Ramda, chain()
Ramda, map()
Ramda, prop()
Ramda, flip()
Ramda, compose()