繁体   English   中英

声明式循环与命令式循环

[英]declarative loop vs imperative loop

我正在尝试将我的编程风格从命令式转换为声明式,但是有一些概念让我感到困扰,比如循环的性能。 例如,我有一个原始的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 次( itemsHashnamesHashrangeItemsHash 哪一个更好? 性能上有什么取舍吗?

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

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

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

我对此有几个回应。

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

如果确实如此,并且您确实需要在单个循环中执行此操作,那么在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调用以证明引用在各种 output 哈希之间共享。)

有两个方向我可能 go 从这里清理这段代码。

首先是使用 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>

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

我可能 go 的另一个方向是注意代码中仍然存在一个潜在的严重性能问题,一个 Rich Snapp 称为reduce ({...spread}) 反模式 为了解决这个问题,我们可能想在 reduce 回调中改变我们的累加器 object。 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点和/或将其内联到主 function 中。 这并不难,另一个答案举了一个例子。 我个人觉得这样更具可读性。 (可能还有更简洁的namesHash版本,但我没时间了。)

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

暂无
暂无

声明:本站的技术帖子网页,遵循CC BY-SA 4.0协议,如果您需要转载,请注明本站网址或者原文地址。任何问题请咨询:yoyou2525@163.com.

 
粤ICP备18138465号  © 2020-2024 STACKOOM.COM