【问题标题】:declarative loop vs imperative loop声明式循环与命令式循环
【发布时间】:2021-07-03 02:33:56
【问题描述】:

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

【问题讨论】:

  • "为了达到预期的效果,命令式只循环一次,而声明式循环多次。" 呃,因为 使声明性代码循环多次。您可以在数据集上使用单个循环进行分组。在 Ramda 中,那是 groupBy/groupWith
  • 1,您可以向我展示获得这 3 个结果的优化代码吗? 2、groupBy可能适合这个例子,但是当涉及到更复杂的场景时,那该怎么办呢?
  • 我的意思是实现rangeItemsHash groupBy 的 1 个单循环就可以了。但是另外两个itemsHash namesHash 呢?他们不采取两行循环吗?

标签: javascript functional-programming ramda.js declarative-programming


【解决方案1】:

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

【讨论】:

  • 谢谢你,先生,真的启发了我!但在这种情况下,你会怎么做才能在现实世界中获得这 3 个结果?你会采取什么解决方案?正如你所说的不值得,你会像我的imperative方式一样,还是你有更好的给我看?
  • 我包括了我将如何使用 Ramda。它循环了DATA 三次,但是(至少对我而言)它立即清楚发生了什么。这基本上就像您自己的尝试,但具有更易读的groupBy 版本的年龄逻辑。我认为免费提供年龄标签功能点不值得。让我更难阅读。
  • 是的,这正是我想问的,因为我认为它至少需要 3 个循环。使用 Ramda 优点:代码看起来更漂亮,更易读,缺点:循环 3 次;不使用 Ramda 优点:循环 1 次,缺点:看起来不那么漂亮;所以我猜真正的问题是任何人都可以使用 Ramda 并循环 1 次吗?如果有人可以,那会是什么?如果没有人可以,值得吗?
  • 我认为更熟悉 Ramda 的人肯定可以创建一个优雅且易于阅读的我用纯 js 编写的组合 reducer 示例的版本。我想这将是吃蛋糕也吃选项?也许你可以自己尝试一下,从reduceBy开始。
【解决方案2】:

我对此有几个回应。

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

如果确实如此,并且您确实需要在单个循环中执行此操作,那么在 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>

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

我可能会采取的另一个方向是注意代码中仍然存在一个潜在的严重性能问题,一个名为 the reduce ({...spread}) anti-pattern 的 Rich Snapp。为了解决这个问题,我们可能想在 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 可能还有更简洁的版本,但我没时间了。)

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

【讨论】:

  • 哇,非常感谢您,先生。看,我知道过分担心花哨的性能问题并不是什么大问题。这里的重点是让我询问在此示例中实现我的目标的最推荐方法是什么。我想我从你那里得到了答案。
  • 另外一个问题,“所以我通常的规则是先写简单明了的代码”,你不觉得我的第一部分代码够简单吗?你愿意这样做而不是用 Ramda 方法编写它吗?
  • @dummy:对我来说,我的最终样本比你的任何一个都简单得​​多。但作为 Ramda 的创始人之一,我确实倾向于考虑它的能力。 YMMV
猜你喜欢
  • 1970-01-01
  • 1970-01-01
  • 2014-06-15
  • 2020-07-22
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
  • 2015-10-24
相关资源
最近更新 更多