從 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
只有 title
與 price
。
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 已各自成型。
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 後,果然越來越接近所要的結果。
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) // ?
由於目前分別由 k
與 v
兩個 array,直覺會想用 zipWith()
將兩個 array 合而為一。
目前結果已經正確,接下來只是重構最佳化
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 也可加以簡化。
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。
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()
增加可讀性。
Conclusion
groupBy()
為本文最關鍵 function,抓到大方向後,就可朝目標邁進,最後再使用pipe()
組合所有 function