函数式编程 RamdaJs groupBy 转换
Functional programming RamdaJs groupBy with transformation
我想创建函数,将数组与特定键分组,如下所示:
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}
基本上按列分组 "name" 并将道具名称设置为具有值的键。
我知道 RamdaJs 中有 groupBy 函数,但它接受生成组密钥的函数。
我知道之后我可以格式化数据,但我会很低效。
有什么方法可以传递某种 "transform" 函数来为每个项目准备数据。
谢谢
我会用 reduceBy
代替:
- 它允许函数生成密钥
- 和一个转换数据的函数
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>
使用通用库和为每个场景编写自定义代码需要权衡取舍。像 Ramda 这样具有数百个函数的库将提供许多可以提供帮助的工具,但它们不可能涵盖所有场景。 Ramda 确实有一个特定的功能,可以将 groupBy
与某种折叠 reduceBy
结合起来。但如果我不知道,我会写一个自定义版本。
我会从有效且简单的开始,只有在测试显示此特定代码存在问题时才担心性能。在这里,我展示了每次更改此类功能以提高性能的多个步骤。我将在这里提出要点:我实际上会坚持使用我的第一个版本,我发现它易于阅读,并且不会为任何性能增强而烦恼,除非我有确凿的数字表明这是我应用程序中的瓶颈。
普通 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。这不应该引起问题,因为它只是我们函数的内部。我们可以写一个不涉及这个的等效版本 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
-loop 写这个:
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
-loop:
的末尾拉出这条线
res[name] = obj;
你在代码中注意到了吗?不是特别难发现,但不一定一眼就能看出来。
总结
在需要时必须非常小心地进行性能优化,因为您无法利用您习惯使用的许多工具。所以,有时它非常重要,然后我就这样做了,但如果我的更清晰、更易于阅读的代码表现得足够好,那么我就会把它留在那里。
Point-free(毫无意义?)撇开
类似的论点适用于对无点代码施加过大压力。这是一种有用的技术,许多功能通过使用它变得更清晰。但它可以超越它的用处。请注意,上述初始版本中的辅助函数 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
循环变体一样难以阅读。
所以,再次强调保持代码简单。这是我的建议,我会坚持的!
命令式 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>
我想创建函数,将数组与特定键分组,如下所示:
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}
基本上按列分组 "name" 并将道具名称设置为具有值的键。
我知道 RamdaJs 中有 groupBy 函数,但它接受生成组密钥的函数。
我知道之后我可以格式化数据,但我会很低效。
有什么方法可以传递某种 "transform" 函数来为每个项目准备数据。
谢谢
我会用 reduceBy
代替:
- 它允许函数生成密钥
- 和一个转换数据的函数
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>
使用通用库和为每个场景编写自定义代码需要权衡取舍。像 Ramda 这样具有数百个函数的库将提供许多可以提供帮助的工具,但它们不可能涵盖所有场景。 Ramda 确实有一个特定的功能,可以将 groupBy
与某种折叠 reduceBy
结合起来。但如果我不知道,我会写一个自定义版本。
我会从有效且简单的开始,只有在测试显示此特定代码存在问题时才担心性能。在这里,我展示了每次更改此类功能以提高性能的多个步骤。我将在这里提出要点:我实际上会坚持使用我的第一个版本,我发现它易于阅读,并且不会为任何性能增强而烦恼,除非我有确凿的数字表明这是我应用程序中的瓶颈。
普通 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。这不应该引起问题,因为它只是我们函数的内部。我们可以写一个不涉及这个的等效版本 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
-loop 写这个:
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
-loop:
res[name] = obj;
你在代码中注意到了吗?不是特别难发现,但不一定一眼就能看出来。
总结
在需要时必须非常小心地进行性能优化,因为您无法利用您习惯使用的许多工具。所以,有时它非常重要,然后我就这样做了,但如果我的更清晰、更易于阅读的代码表现得足够好,那么我就会把它留在那里。
Point-free(毫无意义?)撇开
类似的论点适用于对无点代码施加过大压力。这是一种有用的技术,许多功能通过使用它变得更清晰。但它可以超越它的用处。请注意,上述初始版本中的辅助函数 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
循环变体一样难以阅读。
所以,再次强调保持代码简单。这是我的建议,我会坚持的!
命令式 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>