我正在尝试将我的编程风格从命令式切换到声明式,但是有一些概念困扰着我,例如循环时的性能。例如,我有一个原始数据,在操作它之后,我希望得到 3 个预期结果:itemsHash、namesHash、rangeItemsHash
// original data
const DATA = [
{id: 1, name: 'Alan', date: '2021-01-01', age: 0},
{id: 2, name: 'Ben', date: '1980-02-02', age: 41},
{id: 3, name: 'Clara', date: '1959-03-03', age: 61},
]
...
// expected outcome
// itemsHash => {
// 1: {id: 1, name: 'Alan', date: '2021-01-01', age: 0},
// 2: {id: 2, name: 'Ben', date: '1980-02-02', age: 41},
// 3: {id: 3, name: 'Clara', date: '1959-03-03', age: 61},
// }
// namesHash => {1: 'Alan', 2: 'Ben', 3: 'Clara'}
// rangeItemsHash => {
// minor: [{id: 1, name: 'Alan', date: '2021-01-01', age: 0}],
// junior: [{id: 2, name: 'Ben', date: '1980-02-02', age: 41}],
// senior: [{id: 3, name: 'Clara', date: '1959-03-03', age: 61}],
// }
// imperative way
const itemsHash = {}
const namesHash = {}
const rangeItemsHash = {}
DATA.forEach(person => {
itemsHash[person.id] = person;
namesHash[person.id] = person.name;
if (person.age > 60){
if (typeof rangeItemsHash['senior'] === 'undefined'){
rangeItemsHash['senior'] = []
}
rangeItemsHash['senior'].push(person)
}
else if (person.age > 21){
if (typeof rangeItemsHash['junior'] === 'undefined'){
rangeItemsHash['junior'] = []
}
rangeItemsHash['junior'].push(person)
}
else {
if (typeof rangeItemsHash['minor'] === 'undefined'){
rangeItemsHash['minor'] = []
}
rangeItemsHash['minor'].push(person)
}
})
// declarative way
const itemsHash = R.indexBy(R.prop('id'))(DATA);
const namesHash = R.compose(R.map(R.prop('name')),R.indexBy(R.prop('id')))(DATA);
const gt21 = R.gt(R.__, 21);
const lt60 = R.lte(R.__, 60);
const isMinor = R.lt(R.__, 21);
const isJunior = R.both(gt21, lt60);
const isSenior = R.gt(R.__, 60);
const groups = {minor: isMinor, junior: isJunior, senior: isSenior };
const rangeItemsHash = R.map((method => R.filter(R.compose(method, R.prop('age')))(DATA)))(groups)
为了达到预期的结果,命令式循环只循环一次,而声明式循环至少3次(itemsHash
、namesHash
、rangeItemsHash
)。哪一个更好?在性能上是否有任何权衡?
我对此有几种回应。
首先,您是否测试过是否知道性能是一个问题? 太多的性能工作是在代码上完成的,这些代码甚至没有接近应用程序中的瓶颈。 这通常是以牺牲代码的简单性和清晰度为代价的。 所以我通常的规则是先写简单明了的代码,尽量不要对性能感到愚蠢,但永远不要担心太多。 然后,如果我的应用程序速度慢得令人无法接受,请对其进行基准测试以找出导致最大问题的部分,然后对其进行优化。 我很少让这些地方相当于循环三次而不是一次。 但当然,它可能发生。
如果是这样,并且您确实需要在单个循环中执行此操作,那么在reduce
调用之上执行此操作并不是非常困难。 我们可以写这样的东西:
// helper function
const ageGroup = ({age}) => age > 60 ? 'senior' : age > 21 ? 'junior' : 'minor'
// main function
const convert = (people) =>
people.reduce (({itemsHash, namesHash , rangeItemsHash}, person, _, __, group = ageGroup (person)) => ({
itemsHash: {...itemsHash, [person .id]: person},
namesHash: {...namesHash, [person .id]: person.name},
rangeItemsHash: {...rangeItemsHash, [group]: [...(rangeItemsHash [group] || []), person]}
}), {itemsHash: {}, namesHash: {}, rangeItemsHash: {}})
// sample data
const data = [{id: 1, name: 'Alan', date: '2021-01-01', age: 0}, {id: 2, name: 'Ben', date: '1980-02-02', age: 41}, {id: 3, name: 'Clara', date: '1959-03-03', age: 61}]
// demo
console .log (JSON .stringify (
convert (data)
, null, 4))
.as-console-wrapper {max-height: 100% !important; top: 0}
(可以删除JSON .stringify
调用,以证明引用在各种输出哈希之间共享。
我可能会从这里开始两个方向来清理这段代码。
首先是使用Ramda。 它具有一些功能,可以帮助简化这里的一些事情。 使用R.reduce
,我们可以消除烦人的占位符参数,我用来允许我将默认参数group
添加到 reduce 签名,并保持表达式超过语句样式编码。 (我们也可以用R.call
做一些事情。 并将evolve
与assoc
和over
等函数一起使用,我们可以像这样使其更具声明性:
// helper function
const ageGroup = ({age}) => age > 60 ? 'senior' : age > 21 ? 'junior' : 'minor'
// main function
const convert = (people) =>
reduce (
(acc, person, group = ageGroup (person)) => evolve ({
itemsHash: assoc (person.id, person),
namesHash: assoc (person.id, person.name),
rangeItemsHash: over (lensProp (group), append (person))
}) (acc), {itemsHash: {}, namesHash: {}, rangeItemsHash: {minor: [], junior: [], senior: []}},
people
)
// sample data
const data = [{id: 1, name: 'Alan', date: '2021-01-01', age: 0}, {id: 2, name: 'Ben', date: '1980-02-02', age: 41}, {id: 3, name: 'Clara', date: '1959-03-03', age: 61}]
// demo
console .log (JSON .stringify (
convert (data)
, null, 4))
.as-console-wrapper {max-height: 100% !important; top: 0}
<script src="//cdnjs.cloudflare.com/ajax/libs/ramda/0.27.1/ramda.js"></script>
<script> const {reduce, evolve, assoc, over, lensProp, append} = R </script>
与前一个版本相比,这个版本的一个小缺点是需要在累加器中预定义类别senior
、junior
和minor
。 我们当然可以编写一个以某种方式处理默认值的lensProp
替代方案,但这会让我们走得更远。
我可能要走的另一个方向是注意代码中仍然存在一个潜在的严重性能问题,一个称为reduce({...传播})反模式。 为了解决这个问题,我们可能希望在reduce回调中改变我们的累加器对象。 拉姆达——就其哲学性质而言——不会帮助你解决这个问题。 但是我们可以定义一些帮助程序函数,这些函数将在我们解决此问题的同时清理我们的代码,如下所示:
// utility functions
const push = (x, xs) => ((xs .push (x)), x)
const put = (k, v, o) => ((o[k] = v), o)
const appendTo = (k, v, o) => put (k, push (v, o[k] || []), o)
// helper function
const ageGroup = ({age}) => age > 60 ? 'senior' : age > 21 ? 'junior' : 'minor'
// main function
const convert = (people) =>
people.reduce (({itemsHash, namesHash , rangeItemsHash}, person, _, __, group = ageGroup(person)) => ({
itemsHash: put (person.id, person, itemsHash),
namesHash: put (person.id, person.name, namesHash),
rangeItemsHash: appendTo (group, person, rangeItemsHash)
}), {itemsHash: {}, namesHash: {}, rangeItemsHash: {}})
// sample data
const data = [{id: 1, name: 'Alan', date: '2021-01-01', age: 0}, {id: 2, name: 'Ben', date: '1980-02-02', age: 41}, {id: 3, name: 'Clara', date: '1959-03-03', age: 61}]
// demo
console .log (JSON .stringify (
convert (data)
, null, 4))
.as-console-wrapper {max-height: 100% !important; top: 0}
但最后,正如已经建议的那样,除非性能被证明是一个问题,否则我不会这样做。 我认为像这样的 Ramda 代码要好得多:
const ageGroup = ({age}) => age > 60 ? 'senior' : age > 21 ? 'junior' : 'minor'
const convert = applySpec ({
itemsHash: indexBy (prop ('id')),
nameHash: compose (fromPairs, map (props (['id', 'name']))),
rangeItemsHash: groupBy (ageGroup)
})
const data = [{id: 1, name: 'Alan', date: '2021-01-01', age: 0}, {id: 2, name: 'Ben', date: '1980-02-02', age: 41}, {id: 3, name: 'Clara', date: '1959-03-03', age: 61}]
console .log (JSON .stringify(
convert (data)
, null, 4))
.as-console-wrapper {max-height: 100% !important; top: 0}
<script src="//cdnjs.cloudflare.com/ajax/libs/ramda/0.27.1/ramda.js"></script>
<script> const {applySpec, indexBy, prop, compose, fromPairs, map, props, groupBy} = R </script>
在这里,为了保持一致性,我们可能希望使ageGroup
无点和/或将其内联在主函数中。 这并不难,另一个答案给出了一个例子。 我个人觉得这样可读性更强。 (可能还有一个更干净的namesHash
版本,但我没时间了。
这个版本循环了三次,正是你所担心的。 有时这可能是一个问题。 但我不会花太多精力在这上面,除非这是一个明显的问题。 干净的代码本身就是一个有用的目标。
类似 如何.map(f).map(g) == .map(compose(g, f))
,您可以编写化简器以确保单次传递为您提供所有结果。
编写声明性代码实际上与循环一次或多次的决定没有任何关系。
// Reducer logic for all 3 values you're interested in
// id: person
const idIndexReducer = (idIndex, p) =>
({ ...idIndex, [p.id]: p });
// id: name
const idNameIndexReducer = (idNameIndex, p) =>
({ ...idNameIndex, [p.id]: p.name });
// Age
const ageLabel = ({ age }) => age > 60 ? "senior" : age > 40 ? "medior" : "junior";
const ageGroupReducer = (ageGroups, p) => {
const ageKey = ageLabel(p);
return {
...ageGroups,
[ageKey]: (ageGroups[ageKey] || []).concat(p)
}
}
// Combine the reducers
const seed = { idIndex: {}, idNameIndex: {}, ageGroups: {} };
const reducer = ({ idIndex, idNameIndex, ageGroups }, p) => ({
idIndex: idIndexReducer(idIndex, p),
idNameIndex: idNameIndexReducer(idNameIndex, p),
ageGroups: ageGroupReducer(ageGroups, p)
})
const DATA = [
{id: 1, name: 'Alan', date: '2021-01-01', age: 0},
{id: 2, name: 'Ben', date: '1980-02-02', age: 41},
{id: 3, name: 'Clara', date: '1959-03-03', age: 61},
]
// Loop once
console.log(
JSON.stringify(DATA.reduce(reducer, seed), null, 2)
);
主观部分:是否值得?我不这么认为。我喜欢简单的代码,根据我自己的经验,在处理有限的数据集时,从 1 到 3 个循环通常不明显。
所以,如果使用Ramda,我会坚持:
const { prop, indexBy, map, groupBy, pipe } = R;
const DATA = [
{id: 1, name: 'Alan', date: '2021-01-01', age: 0},
{id: 2, name: 'Ben', date: '1980-02-02', age: 41},
{id: 3, name: 'Clara', date: '1959-03-03', age: 61},
];
const byId = indexBy(prop("id"), DATA);
const nameById = map(prop("name"), byId);
const ageGroups = groupBy(
pipe(
prop("age"),
age => age > 60 ? "senior" : age > 40 ? "medior" : "junior"
),
DATA
);
console.log(JSON.stringify({ byId, nameById, ageGroups }, null, 2))
<script src="https://cdn.jsdelivr.net/npm/ramda@0.27.1/dist/ramda.min.js"></script>