1

我正在尝试将我的编程风格从命令式转换为声明式,但是有一些概念让我感到困扰,比如循环的性能。例如,我有一个原始的DATA,在操纵它之后,我希望得到 3 个预期结果:itemsHashnamesHashrangeItemsHash

// 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 )。哪一个更好?性能上有什么取舍吗?

4

2 回答 2

1

我对此有几个回应。

首先,您是否经过测试知道性能是一个问题?太多的性能工作是在甚至没有接近成为应用程序瓶颈的代码上完成的。这通常以牺牲代码的简单性和清晰度为代价。所以我通常的规则是先写简单明了的代码,尽量不要在性能上犯傻,但不要过分担心。然后,如果我的应用程序速度慢得令人无法接受,请对其进行基准测试以找出导致最大问题的部分,然后对其进行优化。我很少有这些地方相当于循环三次而不是一次。但它当然可能发生。

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.evolveassocover

// 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>

与前一个版本相比,此版本的一个小缺点是需要在累加器中预定义类别seniorjunior和。minor我们当然可以写一个替代方案来lensProp处理默认值,但这会让我们走得更远。

我可能会去的另一个方向是注意代码中仍然存在一个潜在的严重性能问题,一个 Rich Snapp 称为reduce ({...spread}) 反模式。为了解决这个问题,我们可能想在 reduce 回调中改变我们的累加器对象。Ramda——就其哲学性质而言——不会帮助你解决这个问题。但是我们可以定义一些帮助函数,在我们解决这个问题的同时清理我们的代码,如下所示:

// 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,但我没时间了。)

这个版本循环了三遍,正是你所担心的。有时这可能是个问题。但除非这是一个可证明的问题,否则我不会为此付出太多努力。干净的代码本身就是一个有用的目标。

于 2021-04-07T19:14:43.607 回答
1

与 how to 类似.map(f).map(g) == .map(compose(g, f)),您可以编写 reducer 以确保一次通过即可获得所有结果。

编写声明性代码与循环一次或多次的决定没有任何关系。

// 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>

于 2021-04-07T10:55:50.130 回答