函数式编程RamdaJs组通过转换

时间:2019-08-01 12:00:48

标签: ramda.js

我想创建将数组与特定键分组的函数,如下所示:

var items = [
    {name: 'n1', prop: 'p1', value: 90}, 
    {name: 'b', prop: 'p2', value: 1}, 
    {name: 'n1', prop: 'p3', value: 3}];

对此:

{n1: {p1: 90, p3: 3}, {b: {p2: 1}

基本上按“名称”列分组,并将属性名称设置为具有值的键。

我知道RamdaJs中有groupBy函数,但是它接受函数来生成组密钥。

我知道之后可以格式化数据,但是效率不高。

有什么方法可以传递某种“转换”功能来为每一项准备数据。

谢谢

3 个答案:

答案 0 :(得分:3)

在使用通用库并为每种情况编写自定义代码时需要进行权衡。像Ramda这样的具有数百个功能的库将提供许多可以提供帮助的工具,但它们不可能涵盖所有情况。 Ramda确实具有将groupBy与某种折叠方式reduceBy结合使用的特定功能。但是如果我不知道,我会写一个自定义版本。

我将从有效且简单的方法开始,仅在测试显示此特定代码存在问题时担心性能。在这里,我展示了每次更改此类功能以提高性能的许多步骤。我在这里要说明要点:实际上,我会坚持使用我的第一个版本,该版本我很容易阅读,并且不会为任何性能增强而烦恼,除非我有确切的数字表明这是我的应用程序中的瓶颈。 / p>

普通Ramda版本

我的第一遍可能看起来像这样:

const addTo = (obj, {prop, value}) => 
  assoc (prop, value, obj)

const transform1 = pipe (
  groupBy (prop ('name')),
  map (reduce (addTo, {}))
)

const items = [{name: 'n1', prop: 'p1', value: 90}, {name: 'b', prop: 'p2', value: 1}, {name: 'n1', prop: 'p3', value: 3}];

console .log (
  transform1 (items)
)
<script src="//cdnjs.cloudflare.com/ajax/libs/ramda/0.26.1/ramda.js"></script>
<script>const {assoc, pipe, groupBy, prop, map, reduce} = R          </script>

仅循环一次

对我来说,这很清楚而且很容易理解。

但是肯定存在效率问题,因为我们必须遍历该列表进行分组,然后遍历每个组进行折叠。因此,也许我们最好使用自定义函数。这是一个相当简单的现代JS版本:

const transform2 = (items) =>
  items .reduce(
    (a, {name, prop, value}) => ({...a, [name]: {...a[name], [prop]: value}}), 
    {}
  )

const items = [{name: 'n1', prop: 'p1', value: 90}, {name: 'b', prop: 'p2', value: 1}, {name: 'n1', prop: 'p3', value: 3}];

console .log (
  transform2 (items)
)

不要reduce ({...spread})

此版本仅循环一次,这听起来是一个不错的改进……但是对于Rich Snap称为reduce ({...spread}) anti-pattern的性能存在真正的疑问。所以也许我们想使用变异减少。这不应该引起问题,因为它只是我们功能的内部。我们可以编写不涉及此reduce ({...spread}) pattern的等效版本:

const transform3 = (items) =>
  items .reduce (
    (a, {name, prop, value}) => {
      const obj = a [name] || {}
      obj[prop] = value
      a[name] = obj
      return a
    },
    {}
  )

const items = [{name: 'n1', prop: 'p1', value: 90}, {name: 'b', prop: 'p2', value: 1}, {name: 'n1', prop: 'p3', value: 3}];

console .log (
  transform3 (items)
)

更多表演者循环

现在,我们已经删除了该模式(我实际上并不同意它总是一个反模式),我们有一些性能更高的代码,但是我们仍然可以做一件事。众所周知,Array.prototype之类的功能reduce并不比它们的普通循环功能快。因此,我们可以更进一步,并使用for循环编写此代码:

const transform4 = (items) => {
  const res = {}
  for (let i = 0; i < items .length; i++) {
    const {name, prop, value} = items [i]
    const obj = res [name] || {}
    obj[prop] = value
  }
  return res
}

const items = [{name: 'n1', prop: 'p1', value: 90}, {name: 'b', prop: 'p2', value: 1}, {name: 'n1', prop: 'p3', value: 3}];  console.log('This version is intentionally broken.  See the text for the fix.');

console .log (
  transform4 (items)
)

在性能优化方面,我们已经达到了我能想到的极限。

...而且我们使代码更糟!将最后一个版本与第一个版本

进行比较
const transform1 = pipe (
  groupBy (prop ('name')),
  map (reduce (addTo, {}))
)

在代码清晰性方面,我们看到了毫无疑问的赢家。在不了解addTo助手的详细信息的情况下,我们仍然可以预先了解该函数在初读时的作用。如果我们希望这些细节更加明显,我们可以简单地内联该帮助程序。不过,版本将需要仔细阅读以了解其工作原理。

哦,等等;它不起作用。您是否测试过并看到了?你看到什么丢失了吗?我从for循环的结尾拉出了这一行:

    res[name] = obj;

您在代码中注意到了吗?发现它并不是特别困难,但是乍一看并不一定很明显。

摘要

性能优化在需要时必须非常小心地完成,因为您无法利用许多习惯使用的工具。因此,有时候它非常重要,然后我就去做,但是如果我的简洁,易于阅读的代码表现良好,那么我就把它留在那里。


无点(毫无意义?)放在一边

类似的论点适用于对无点代码进行过分的推动。这是一种有用的技术,使用它会使许多功能变得更加简洁。但是它可以超出其用途。请注意,上述初始版本中的辅助函数addTo并非没有意义。我们可以为其制作无积分版本。可能有更简单的方法,但是我想到的第一件事是pipe (lift (objOf) (prop ('prop'), prop ('value')), mergeAll)。我们可以通过以下方式内联地编写此函数的完全无点版本:

const transform5 = pipe (
  groupBy (prop ('name')),
  map (pipe (
    map (lift (objOf) (
      prop ('prop'), 
      prop ('value')
    )), 
    mergeAll
  ))
)

const items = [{name: 'n1', prop: 'p1', value: 90}, {name: 'b', prop: 'p2', value: 1}, {name: 'n1', prop: 'p3', value: 3}];

console .log (
  transform5 (items)
)
<script src="//cdnjs.cloudflare.com/ajax/libs/ramda/0.26.1/ramda.js"></script>
<script>const {pipe, groupBy, prop, map, lift, objOf, mergeAll} = R  </script>

这能给我们带来什么好处吗?我看不到。代码更复杂,表达能力也更差。这和for循环变体一样难以理解。

因此,再次强调保持代码简单。这是我的建议,我坚持下去!

答案 1 :(得分:2)

我会改用reduceBy

  1. 它允许函数生成密钥
  2. 以及用于转换数据的功能

const items = [
  {name: 'n1', prop: 'p1', value: 90}, 
  {name: 'b', prop: 'p2', value: 1}, 
  {name: 'n1', prop: 'p3', value: 3}];

// {name: 'n1', prop: 'p1', value: 90} => {p1: 90}
const kv = obj => ({[obj.prop]: obj.value});

// {p1: 90}, {name: 'n1', prop: 'p3', value: 3} -> {p1: 90, p3: 3}
const reducer = (acc, obj) => mergeRight(acc, kv(obj));

console.log(
  
  reduceBy(reducer, {}, prop('name'), items)

)
<script src="https://cdnjs.cloudflare.com/ajax/libs/ramda/0.26.1/ramda.min.js"></script>
<script>const {reduceBy, prop, mergeRight} = R;</script>

答案 2 :(得分:1)

一个命令性的for...of循环,虽然有点冗长,但是性能却很不错。它具有可读性。

const fn = arr => {
  const obj = {}
  
  for(const { name, prop, value } of arr) {
    if(!obj[name]) obj[name] = {} // initialize the group if it doesn't exist
    
    obj[name][prop] = value // add the prop and it's value to the group
  }
  
  return obj
}

const items = [{name: 'n1', prop: 'p1', value: 90}, {name: 'b', prop: 'p2', value: 1}, {name: 'n1', prop: 'p3', value: 3}]

const result = fn(items)

console.log(result)

使用Ramda的功能性解决方案速度较慢,但​​根据阵列中项目的数量,可以忽略不计。我通常从功能性解决方案开始,并且只有在遇到性能问题时,才进行配置,然后回退到性能更高的强制性选项。

使用Ramda的可读无点解决方案-R.groupBy和R.map将成为基础。在这种情况下,我将每个组项目映射到其道具,然后使用R.fromPairs生成对象。

const { pipe, groupBy, prop, map, props, fromPairs } = R

const fn = pipe(
  groupBy(prop('name')),
  map(pipe(
    map(props(['prop', 'value'])),
    fromPairs
  ))
)

const items = [{name: 'n1', prop: 'p1', value: 90}, {name: 'b', prop: 'p2', value: 1}, {name: 'n1', prop: 'p3', value: 3}]

const result = fn(items)

console.log(result)
<script src="//cdnjs.cloudflare.com/ajax/libs/ramda/0.26.1/ramda.js"></script>