點燈坊

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

如何將資料巢狀分組 ?

Sam Xiao's Avatar 2020-02-10

從 SQL 回傳資料為扁平狀,但實務上 API 常會回傳分組後且帶有巢狀資料,這種常見的需求該如何使用 Ramda 完成呢 ?

Version

macOS Mojave 10.15.3
VS Code 1.41.1
Quokka 1.0.277
Ramda 0.26.1

Scenario

let data = [
  { type: 'A', title: 'FP in JavaScript', price: 100 },
  { type: 'A', title: 'RxJS in Action', price: 200 },
  { type: 'B', title: 'Speaking JavaScript', price: 300 },
  { type: 'B', title: 'Programming in Haskell', price: 400 },
  { type: 'C', title: 'Learn Haskell', price: 500 },
  { type: 'C', title: 'Real World Haskell', price: 600 },
]

let result = [
  { 
    type: 'A', 
    books: [
      { title: 'FP in JavaScript', price: 100 },
      { title: 'RxJS in Action', price: 200 }
    ]
  },
  {
    type: 'B',
    books: [
      { title: 'Speaking JavaScript', price: 300 },
      { title: 'Programming in Haskell', price: 400 }
    ]
  },
  {
    type: 'C',
    books: [
      { title: 'Learn Haskell', price: 500 },
      { title: 'Real World Haskell', price: 600 }
    ]
  }
]

由 SQL 抓出來的資料如同 data,但最後 API 回傳會將 type 做整理,相同 type 歸於一筆,而 books 只有 titleprice

groupBy()

import { groupBy } from 'ramda'

let data = [
  { type: 'A', title: 'FP in JavaScript', price: 100 },
  { type: 'A', title: 'Rx in Action', price: 200 },
  { type: 'B', title: 'Speaking JavaScript', price: 300 },
  { type: 'B', title: 'Get Programming in Haskell', price: 400 },
  { type: 'C', title: 'Learn Haskell', price: 500 },
  { type: 'C', title: 'Real World Haskell', price: 600 },
]

let f = groupBy(({ type }) => {
  return type === 'A' ? 'A' :
         type === 'B' ? 'B' : 'C'
})

f(data) // ?

由於要分組,Ramda 中最接近的就是 groupBy(),但結果為 object,但最少 key 與 value 已各自成型。

groupby000

keys() / values()

import { groupBy, keys, values } from 'ramda'

let data = [
  { type: 'A', title: 'FP in JavaScript', price: 100 },
  { type: 'A', title: 'Rx in Action', price: 200 },
  { type: 'B', title: 'Speaking JavaScript', price: 300 },
  { type: 'B', title: 'Get Programming in Haskell', price: 400 },
  { type: 'C', title: 'Learn Haskell', price: 500 },
  { type: 'C', title: 'Real World Haskell', price: 600 },
]

let f = groupBy(({ type }) => {
  return type === 'A' ? 'A' :
         type === 'B' ? 'B' : 'C'
})

let obj = f(data)

let k = keys(obj) // ?
let v = values(obj) // ?

使用 keys()values() 各自轉成 array 後,果然越來越接近所要的結果。

groupby001

zipWith()

import { groupBy, keys, values, zipWith } from 'ramda'

let data = [
  { type: 'A', title: 'FP in JavaScript', price: 100 },
  { type: 'A', title: 'Rx in Action', price: 200 },
  { type: 'B', title: 'Speaking JavaScript', price: 300 },
  { type: 'B', title: 'Get Programming in Haskell', price: 400 },
  { type: 'C', title: 'Learn Haskell', price: 500 },
  { type: 'C', title: 'Real World Haskell', price: 600 },
]

let f = groupBy(({ type }) => {
  return type === 'A' ? 'A' :
         type === 'B' ? 'B' : 'C'
})

let obj = f(data)

let k = keys(obj)
let v = values(obj)

let result = zipWith((x, y) => ({
  type: x,
  books: y
}))(k)(v) // ?                 

由於目前分別由 kv 兩個 array,直覺會想用 zipWith() 將兩個 array 合而為一。

目前結果已經正確,接下來只是重構最佳化

groupby002

pipe()

import { groupBy, keys, values, zipWith, pipe, converge } from 'ramda'

let data = [
  { type: 'A', title: 'FP in JavaScript', price: 100 },
  { type: 'A', title: 'Rx in Action', price: 200 },
  { type: 'B', title: 'Speaking JavaScript', price: 300 },
  { type: 'B', title: 'Get Programming in Haskell', price: 400 },
  { type: 'C', title: 'Learn Haskell', price: 500 },
  { type: 'C', title: 'Real World Haskell', price: 600 },
]

let f = pipe(
  groupBy(x => x.type),
  converge(
    zipWith((x, y) => ({ type: x, books: y })), 
    [keys, values]
  )
)

f(data) // ?

可使用 pipe() 將所有流程串起來,groupBy() 的 callback 也可加以簡化。

groupby003

Point-free

import { groupBy, keys, values, zipWith, pipe, converge, prop } from 'ramda'

let data = [
  { type: 'A', title: 'FP in JavaScript', price: 100 },
  { type: 'A', title: 'Rx in Action', price: 200 },
  { type: 'B', title: 'Speaking JavaScript', price: 300 },
  { type: 'B', title: 'Get Programming in Haskell', price: 400 },
  { type: 'C', title: 'Learn Haskell', price: 500 },
  { type: 'C', title: 'Real World Haskell', price: 600 },
]

let f = pipe(
  groupBy(prop('type')),
  converge(
    zipWith((x, y) => ({ type: x, books: y })),
    [keys, values]
  )
)

f(data) // ?

groupBy() 的 callback 可再使用 prop() point-free。

groupby004

Refactoring

import { groupBy, keys, values, zipWith, pipe, converge, prop } from 'ramda'

let data = [
  { type: 'A', title: 'FP in JavaScript', price: 100 },
  { type: 'A', title: 'Rx in Action', price: 200 },
  { type: 'B', title: 'Speaking JavaScript', price: 300 },
  { type: 'B', title: 'Get Programming in Haskell', price: 400 },
  { type: 'C', title: 'Learn Haskell', price: 500 },
  { type: 'C', title: 'Real World Haskell', price: 600 },
]

let zipper = (x, y) => ({ type: x, books: y })

let f = pipe(
  groupBy(prop('type')),
  converge(zipWith(zipper), [keys, values])
)

f(data) // ?

可將 zipWith() 的 callback 抽成 zipper() 增加可讀性。

groupby005

Conclusion

  • groupBy() 為本文最關鍵 function,抓到大方向後,就可朝目標邁進,最後再使用 pipe() 組合所有 function