简体   繁体   中英

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

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:

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

You can use R.apply spec to create the object. For each property get the values from the array using 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:

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

You can use mergeWith to merge two objects. When there is a collision (eg a key exists in both objects) a function is applied to both values:

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.

The unapply(flatten)(a, b) trick does this under the hood: 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:

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 .

There are already several good ways to solve this. The one-liners from customcommander and jmw are quite impressive. 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.)

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. 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. Let's call it recompose , and build it like this:

 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. Each one gets the result of the preceding call (starting of course with a ) and the value of b .

This may be all you need, but let's note a few things about this function. First of all, although we gave it a name cognate with compose , this is really a version of pipe . We call the functions from the first to the last. compose goes the other direction. We can fix this easily enough by replacing reduce with 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.

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:

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

{
  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. So there is at least a potential problem with the reducer functions. There is also no need that I can see for the curry wrapper on them.

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 . 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. (On the other hand, I could see Ramda adopting the recompose function, as it seems generally useful.)

The technical post webpages of this site follow the CC BY-SA 4.0 protocol. If you need to reprint, please indicate the site URL or the original address.Any question please contact:yoyou2525@163.com.

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