简体   繁体   English

如何在 Ramda 中组合多个减速器?

[英]How can I combine multiple reducers in Ramda?

I'm trying to build reports by combining several different functions.我正在尝试通过组合几个不同的功能来构建报告。 I've been able to get what I want using some vanilla javascript but it's way too wonky and I know I'd be better off if I can use a library.我已经能够使用一些香草 javascript 获得我想要的东西,但这太不稳定了,我知道如果我可以使用图书馆我会更好。 Ramda seems right but I've hit a road block and I would appreciate it if someone could give me a push in the right direction. Ramda 似乎是对的,但我遇到了障碍,如果有人能给我推动正确的方向,我将不胜感激。

I'll be importing functions from different files and stitching them together at the last minute for the report I need.我将从不同文件中导入函数,并在最后一分钟将它们拼接在一起以生成我需要的报告。

Let's pretend this is my code:让我们假设这是我的代码:

const data = [
  { name: 'fred', age: 30, hair: 'black' },
  { name: 'wilma', age: 28, hair: 'red' },
  { name: 'barney', age: 29, hair: 'blonde' },
  { name: 'betty', age: 26, hair: 'black' }
]
const partA = curry((acc, thing) => {
  if (!acc.names) acc.names = [];
  acc.names.push(thing.name);
  return acc;
})
const partB = curry((acc, thing) => {
  if (!acc.ages) acc.ages = [];
  acc.ages.push(thing.age);
  return acc;
})
const partC = curry((acc, thing) => {
  if (!acc.hairColors) acc.hairColors = [];
  acc.hairColors.push(thing.hair);
  return acc;
})

I can't seem to figure out a good way to squash the partA + partB + partC functions together so that I get this:我似乎无法找到一种将 partA + partB + partC 函数压缩在一起的好方法,以便我得到这个:

{
    ages: [30, 28, 29, 26],
    hairColors: ["black", "red", "blonde", "black"],
    names: ["fred", "wilma", "barney", "betty"]
}

This works but it's horrible.这有效,但很可怕。

reduce(partC, reduce(partB, reduce(partA, {}, data), data), data)

Here's one I can live with but I'm sure it can't be right.这是我可以忍受的一个,但我确信它不可能是正确的。

const allThree = (acc, thing) => {
  return partC(partB(partA(acc, thing), thing), thing)
}
reduce(allThree, {}, data)

I've tried compose, pipe, reduce, reduceRight and into as well as some others so obviously I'm missing something pretty fundamental here.我已经尝试过 compose、pipe、reduce、reduceRight 和 into 以及其他一些,所以很明显我在这里遗漏了一些非常基本的东西。

You can use R.apply spec to create the object.您可以使用 R.apply 规范创建 object。 For each property get the values from the array using R.pluck:对于每个属性,使用 R.pluck 从数组中获取值:

 const { pluck, applySpec } = R const fn = applySpec({ ages: pluck('age'), hairColors: pluck('hair'), named: pluck('name'), }) const data =[{"name":"fred","age":30,"hair":"black"},{"name":"wilma","age":28,"hair":"red"},{"name":"barney","age":29,"hair":"blonde"},{"name":"betty","age":26,"hair":"black"}] const result = fn(data) console.log(result)
 <script src="https://cdnjs.cloudflare.com/ajax/libs/ramda/0.27.1/ramda.min.js" integrity="sha512-rZHvUXcc1zWKsxm7rJ8lVQuIr1oOmm7cShlvpV0gWf0RvbcJN6x96al/Rp2L2BI4a4ZkT2/YfVe/8YvB2UHzQw==" crossorigin="anonymous"></script>

A more generic version accepts an object with the future property name, and the key to pluck, maps the object to curry pluck with the relevant key, and then uses it with R.applySpec:更通用的版本接受具有未来属性名称的 object 和要采摘的键,将 object 映射到带有相关键的咖喱采摘,然后将其与 ZE1E1D3D40573127E9EE0480CAF1283DZ 一起使用:

 const { pipe, map, pluck, applySpec } = R const getArrays = pipe(map(pluck), applySpec) const fn = getArrays({ 'ages': 'age', hairColors: 'hair', names: 'name' }) const data =[{"name":"fred","age":30,"hair":"black"},{"name":"wilma","age":28,"hair":"red"},{"name":"barney","age":29,"hair":"blonde"},{"name":"betty","age":26,"hair":"black"}] const result = fn(data) console.log(result)
 <script src="https://cdnjs.cloudflare.com/ajax/libs/ramda/0.27.1/ramda.min.js" integrity="sha512-rZHvUXcc1zWKsxm7rJ8lVQuIr1oOmm7cShlvpV0gWf0RvbcJN6x96al/Rp2L2BI4a4ZkT2/YfVe/8YvB2UHzQw==" crossorigin="anonymous"></script>

You can do the same with vanilla JS using Array.map() to pluck a value from the array's items.您可以对 vanilla JS 执行相同操作,使用Array.map()从数组的项目中提取一个值。 To use a spec object, convert to entries using Object.entries() , map each array to get the relevant value, and then convert back to an object using Object.fromEntries() : To use a spec object, convert to entries using Object.entries() , map each array to get the relevant value, and then convert back to an object using Object.fromEntries() :

 const getArrays = keys => arr => Object.fromEntries( Object.entries(keys).map(([keyName, key]) => [keyName, arr.map(o => o[key])]) ); const fn = getArrays({ 'ages': 'age', hairColors: 'hair', names: 'name' }) const data =[{"name":"fred","age":30,"hair":"black"},{"name":"wilma","age":28,"hair":"red"},{"name":"barney","age":29,"hair":"blonde"},{"name":"betty","age":26,"hair":"black"}] const result = fn(data) console.log(result)

I would use reduce with mergeWith :我会使用reducemergeWith

You can use mergeWith to merge two objects.您可以使用mergeWith来合并两个对象。 When there is a collision (eg a key exists in both objects) a function is applied to both values:当发生冲突时(例如,两个对象中都存在一个键) function 应用于两个值:

mergeWith(add, {a:1,b:2}, {a:10,b:10});
//=> {a:11,b:12}

So in a reducer, your first object is the accumulator (starts with {} ) and the second object is taken from the list at each iteration.因此,在减速器中,您的第一个 object 是累加器(以{}开头),第二个 object 在每次迭代时从列表中取出。

The unapply(flatten)(a, b) trick does this under the hood: flatten([a, b]) . unapply(flatten)(a, b)技巧在后台执行此操作: flatten([a, b])

 const compact = reduce(mergeWith(unapply(flatten)), {}); console.log( compact([ { name: 'fred', age: 30, hair: 'black' }, { name: 'wilma', age: 28, hair: 'red' }, { name: 'barney', age: 29, hair: 'blonde' }, { name: 'betty', age: 26, hair: 'black' } ] ));
 <script src="https://cdnjs.cloudflare.com/ajax/libs/ramda/0.27.1/ramda.min.js" integrity="sha512-rZHvUXcc1zWKsxm7rJ8lVQuIr1oOmm7cShlvpV0gWf0RvbcJN6x96al/Rp2L2BI4a4ZkT2/YfVe/8YvB2UHzQw==" crossorigin="anonymous"></script> <script>const {reduce, mergeWith, unapply, flatten} = R;</script>

What you describe as horrible is actually the general way to do it, but it assumes your reductions are independent of each other.您所描述的可怕实际上是执行此操作的一般方法,但它假设您的减少是相互独立的。 In this case they are not.在这种情况下,它们不是。

For the specific case you describe, I like the reduce(mergeWith) method, though I would use less trickery:对于您描述的特定情况,我喜欢reduce(mergeWith)方法,尽管我会使用较少的技巧:

const compact = R.compose( R.reduce(R.mergeWith(R.concat))({}), R.map(R.map(R.of)) );

map(map(of)) changes name: "fred" to name: ["fred"] so the values can be merged with concat . map(map(of))name: "fred"更改为name: ["fred"] ,因此这些值可以与concat合并。

There are already several good ways to solve this.已经有几种很好的方法可以解决这个问题。 The one-liners from customcommander and jmw are quite impressive.来自 customcommander 和 jmw 的单行代码令人印象深刻。 I prefer the applySpec solution from OriDrori, though, as it seems much more obvious what's going on (and unlike the other two, it allows you to directly do the field-name change you request ("hair" => "hairColors", etc.)不过,我更喜欢 OriDrori 的applySpec解决方案,因为它看起来更明显(与其他两个不同,它允许您直接更改您请求的字段名称(“hair” => “hairColors”等) .)

But let's assume that you really are looking more for how to do the sort of composition you want with these three functions only as examples.但是让我们假设您确实在寻找更多的方法来寻找如何使用这三个函数来做您想要的组合,这只是作为示例。

The reason they don't compose the way you would like is that all of them take two parameters.它们不按您想要的方式组合的原因是它们都采用两个参数。 You want to pass the changing accumulator and the individual thing to each function.您想将不断变化的累加器和单个事物传递给每个 function。 Typical composition only passes one parameter along (except possibly for the first function called.) R.compose and R.pipe simply won't do what you want. Typical composition only passes one parameter along (except possibly for the first function called.) R.compose and R.pipe simply won't do what you want.

But it's quite simple to write our own composition function.但是写我们自己的作文function相当简单。 Let's call it recompose , and build it like this:我们称它为recompose ,并像这样构建它:

 const recompose = (...fns) => (a, b) => fns.reduce ((v, fn) => fn (v, b), a) const partA = curry((acc, thing) => {if (.acc.names) acc;names = []. acc.names.push(thing;name); return acc,}) const partB = curry((acc. thing) => {if (.acc;ages) acc.ages = []. acc.ages;push(thing;age), return acc.}) const partC = curry((acc. thing) => {if (;acc.hairColors) acc.hairColors = []. acc;hairColors;push(thing,hair), return acc,}) const compact = data => reduce (recompose (partA, partB: partC), {}: data) const data = [{ name, 'fred': age, 30: hair, 'black' }: { name, 'wilma': age, 28: hair, 'red' }: { name, 'barney': age, 29: hair, 'blonde' }: { name, 'betty': age. 26, hair: 'black' }] console .log (compact (data))
 .as-console-wrapper {max-height: 100%;important: top: 0}
 <script src="https://cdnjs.cloudflare.com/ajax/libs/ramda/0.27.1/ramda.min.js"></script> <script>const {reduce, curry} = R </script>

The recompose function passes a second parameter to all of our composed functions. recompose function 将第二个参数传递给我们所有的组合函数。 Each one gets the result of the preceding call (starting of course with a ) and the value of b .每个人都得到前一个调用的结果(当然从a开始)和b的值。

This may be all you need, but let's note a few things about this function.这可能就是您所需要的,但是让我们注意一些关于这个 function 的事情。 First of all, although we gave it a name cognate with compose , this is really a version of pipe .首先,虽然我们给它起了一个与compose同源的名字,但这实际上是pipe的一个版本。 We call the functions from the first to the last.我们从第一个到最后一个调用函数。 compose goes the other direction. compose走向另一个方向。 We can fix this easily enough by replacing reduce with reduceRight .我们可以通过将reduce替换为reduceRight来轻松解决这个问题。 Second, we may want to pass through a third argument and maybe a fourth, and so on.其次,我们可能想要传递第三个参数,也许还有第四个参数,等等。 It might be nice if we handled that.如果我们处理它可能会很好。 We can, quite easily, through rest parameters.我们可以很容易地通过 rest 参数。

Fixing those two, we get修复这两个,我们得到

const recompose = (...fns) => (a, ...b) => 
  fns .reduceRight ((v, fn) => fn (v, ...b), a)

There is another potential concern here.这里还有另一个潜在的问题。

This was necessary:这是必要的:

const compact = data => reduce (recompose (partA, partB, partC), {}, data)

even though with Ramda, we traditionally do this:即使使用 Ramda,我们传统上也会这样做:

const compact = reduce (recompose (partA, partB, partC), {})

The reason is that your reducing functions all modify the accumulator.原因是您的归约函数都修改了累加器。 If we used the latter, and then ran compact (data) , we would get如果我们使用后者,然后运行compact (data) ,我们会得到

{
  ages: [30, 28, 29, 26], 
  hairColors: ["black", "red", "blonde", "black"], 
  names: ["fred", "wilma", "barney", "betty"]
}

which is fine, but if we called it again, we would get这很好,但如果我们再次调用它,我们会得到

{
  ages: [30, 28, 29, 26, 30, 28, 29, 26], 
  hairColors: ["black", "red", "blonde", "black", "black", "red", "blonde", "black"], 
  names: ["fred", "wilma", "barney", "betty", "fred", "wilma", "barney", "betty"]
}

which might be a bit problematic.这可能有点问题。 :-) The trouble is that there is only the one accumulator in the definition, which usually in Ramda is not a problem, but here when we modify the accumulator, we can get real issues. :-) 麻烦的是定义中只有一个累加器,这在 Ramda 中通常是没有问题的,但是当我们修改累加器时,我们可以得到真正的问题。 So there is at least a potential problem with the reducer functions.因此,reducer 函数至少存在一个潜在问题。 There is also no need that I can see for the curry wrapper on them.我也不需要看到它们上面的curry皮。

I would suggest rewriting them to return a new value rather than mutating the accumulator.我建议重写它们以返回一个新值,而不是改变累加器。 Here's one possibility to rewrite the hair reducer:这是重写头发减少器的一种可能性:

const partC = (acc, {hair}) => ({
  ...acc, 
  hairColors: [...(acc.hairColors || []), hair]
})

We should note that this is less efficient than the original, but it's significantly cleaner.我们应该注意到,这比原来的效率低,但它明显更清洁。


This solution, although it uses Ramda, does so very lightly, really only using reduce .这个解决方案,虽然它使用 Ramda,但做起来非常轻松,实际上只使用reduce I'm one of the founders of Ramda, and a big fan, but modern JS often reduces the need for a library like this to solve this sort of problem.我是 Ramda 的创始人之一,也是一个忠实的粉丝,但现代 JS 通常会减少对此类库的需求来解决此类问题。 (On the other hand, I could see Ramda adopting the recompose function, as it seems generally useful.) (另一方面,我可以看到 Ramda 采用了recompose function,因为它似乎通常很有用。)

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

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