简体   繁体   English

声明式循环与命令式循环

[英]declarative loop vs imperative loop

I am trying to switch my programming style to declarative from imperative , but there is some concept that is bugging me like the performance when it comes to the loop .我正在尝试将我的编程风格从命令式转换为声明式,但是有一些概念让我感到困扰,比如循环的性能。 For example, I have an original DATA , and after manipulating it I wish to get 3 expected outcomes: itemsHash , namesHash , rangeItemsHash例如,我有一个原始的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)

To achieve the expected outcome, imperative only loops once while declarative loops at least 3 times( itemsHash , namesHash , rangeItemsHash ) .为了达到预期的结果,命令式只循环一次,而声明式循环至少 3 次( itemsHashnamesHashrangeItemsHash Which one is better?哪一个更好? Is there any trade-off on performance?性能上有什么取舍吗?

Similar how to .map(f).map(g) ==.map(compose(g, f)) , you can compose reducers to ensure a single pass gives you all results..map(f).map(g) ==.map(compose(g, f))类似,您可以编写 reducers 以确保单次通过即可获得所有结果。

Writing declarative code does not really have anything to do with the decision to loop once or multiple times.编写声明性代码与循环一次或多次的决定没有任何关系。

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

Subjective part: Whether it's worth it?主观部分:是否值得? I don't think so.我不这么认为。 I like simple code, and in my own experience going from 1 to 3 loops when working with limited data sets usually is unnoticeable.我喜欢简单的代码,根据我自己的经验,在处理有限的数据集时,从 1 到 3 个循环通常是不引人注意的。

So, if using Ramda, I'd stick to:所以,如果使用 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>

I have several responses to this.我对此有几个回应。

First, have you tested to know that performance is a problem?首先,您是否经过测试知道性能是一个问题? Far too much performance work is done on code that is not even close to being a bottleneck in an application.太多的性能工作是在甚至没有接近成为应用程序瓶颈的代码上完成的。 This often happens at the expense of code simplicity and clarity.这通常以牺牲代码的简单性和清晰度为代价。 So my usual rule is to write the simple and obvious code first, trying not to be stupid about performance, but never worrying overmuch about it.所以我通常的规则是先写简单明了的代码,尽量不要在性能上犯傻,但不要过分担心。 Then, if my application is unacceptably slow, benchmark it to find what parts are causing the largest issues, then optimize those.然后,如果我的应用程序速度慢得令人无法接受,请对其进行基准测试以找出导致最大问题的部分,然后对其进行优化。 I've rarely had those places be the equivalent of looping three times rather than one.我很少有这些地方相当于循环三次而不是一次。 But of course it could happen.但它当然可能发生。

If it does, and you really need to do this in a single loop, it's not terribly difficult to do this on top of a reduce call.如果确实如此,并且您确实需要在单个循环中执行此操作,那么在reduce调用之上执行此操作并不难。 We could write something like this:我们可以这样写:

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

(You can remove the JSON.stringify call to demonstrate that the references are shared between the various output hashes.) (您可以删除JSON.stringify调用以证明引用在各种 output 哈希之间共享。)

There are two directions I might go from here to clean up this code.有两个方向我可能 go 从这里清理这段代码。

The first would be to use Ramda.首先是使用 Ramda。 It has some functions that would help simplify a few things here.它有一些功能可以帮助简化这里的一些事情。 Using R.reduce , we could eliminate the annoying placeholder parameters that I use to allow me to add the default parameter group to the reduce signature, and maintain expressions-over-statements style coding.使用R.reduce ,我们可以消除烦人的占位符参数,我使用这些参数可以将默认参数group添加到 reduce 签名,并保持表达式超过语句的样式编码。 (We could alternatively do something with R.call .) And using evolve together with functions like assoc and over , we can make this more declarative like this: (我们也可以使用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>

A slight downside to this version over the previous one is the need to predefine the categories senior , junior , and minor in the accumulator.与前一个版本相比,此版本的一个小缺点是需要在累加器中预定义seniorjuniorminor类别。 We could certainly write an alternative to lensProp that somehow deals with default values, but that would take us further afield.我们当然可以写一个以某种方式处理默认值的lensProp替代方案,但这会让我们走得更远。

The other direction I might go is to note that there is still one potentially serious performance problem in the code, one Rich Snapp called the reduce ({...spread}) anti-pattern .我可能 go 的另一个方向是注意代码中仍然存在一个潜在的严重性能问题,一个 Rich Snapp 称为reduce ({...spread}) 反模式 To solve that, we might want to mutate our accumulator object in the reduce callback.为了解决这个问题,我们可能想在 reduce 回调中改变我们的累加器 object。 Ramda -- by its very philosophic nature -- will not help you with this. Ramda——就其哲学性质而言——不会帮助你解决这个问题。 But we can define some helper functions that will clean our code up at the same time we address this issue, with something like this:但是我们可以定义一些帮助函数,在我们解决这个问题的同时清理我们的代码,如下所示:

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

But in the end, as already suggested, I would not do this unless performance was provably a problem.但最后,正如已经建议的那样,除非性能被证明是一个问题,否则我不会这样做。 I think it's much nicer with Ramda code like this:我认为像这样的 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>

Here we might want -- for consistency's sake -- to make ageGroup point-free and/or inline it in the main function.在这里,为了保持一致性,我们可能希望将ageGroup点和/或将其内联到主 function 中。 That's not hard, and another answer gave an example of that.这并不难,另一个答案举了一个例子。 I personally find it more readable like this.我个人觉得这样更具可读性。 (There's also probably a cleaner version of namesHash , but I'm out of time.) (可能还有更简洁的namesHash版本,但我没时间了。)

This version loops three times, exactly what you are worried about.这个版本循环了三遍,正是你所担心的。 There are times when that might be a problem.有时这可能是个问题。 But I wouldn't spend much effort on that unless it's a demonstrable problem.但除非这是一个可证明的问题,否则我不会为此付出太多努力。 Clean code is a useful goal on its own.干净的代码本身就是一个有用的目标。

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

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