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.
Advertisement
Answer
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.)