Skip to content
Advertisement

Passing _.groupBy to _.partialRight seems to give incorrect results

Here’s a JavaScript object,

const obj = {a: [{ id: 1 }, {id: 1}, {id: 2}, {id: 3}], b: [{ id: 4 }, {id: 5}, {id: 5}, {id: 6}] };

and here’s a code that correctly groups the items by .id in each of the two arrays ojb.a and obj.b,

const res1 = _.map(obj, x => _.groupBy(x, 'id'));

the result being

[
 {
  1: [{id: 1}, {id: 1}],
  2: [{id: 2}],
  3: [{id: 3}]
 },
 {
  4: [{id: 4}],
  5: [{id: 5}, {id: 5}],
  6: [{id: 6}]
 }
]

The lambda, however, is in fact just the partial application of _.groupBy to its second argument, which is set to 'id', so I thought something like this should work,

const res2 = _.map(obj, _.partialRight(_.groupBy, 'id'));

or at least something like this

const res2 = _.map(obj, _.partialRight(_.groupBy, x => x.id));

however, neither of them works, both resulting in this object:

[
 {
   undefined: [{id: 1}, {id: 1}, {id: 2}, {id: 3}]
 },
 {
   undefined: [{id: 4}, {id: 5}, {id: 5}, {id: 6}]
 }
]

Why is that? Is it a bug in lodash? Or is it because of how JavaScript works? In the latter case, what’s happening?


I’ve found an existing question + self-answer which gives a solution for making the code above work:

const res2 = _.map(obj, _.ary(_.partialRight(_.groupBy, 'id'), 1));

However part of my question is still not answerd: why do I need to use _.ary? Why doesn’t my initial attempt work?

Advertisement

Answer

The _.partialRight method can still accept more arguments than the new function should expect. If a function takes two arguments, has one or two partially applied, then any extra arguments effectively “bump off” the partially applied ones:

function print(a, b) {
  console.log(a, b);
}

const f = _.partialRight(print, "world");
const g = _.partialRight(print, "hello", "world");

f("hi");                    // hi world
g();                        // hello world

f("hi", "universe");        // hi universe
g("greetings");             // greetings world
g("greetings", "universe"); // greetings universe
<script src="https://cdn.jsdelivr.net/npm/lodash@4.17.21/lodash.min.js"></script>

This happens because _.partialRight effectively adds to the end of the arguments object:

function print(a, b) {
  console.log(...arguments);
}

const f = _.partialRight(print, "world");
const g = _.partialRight(print, "hello", "world");

f("hi");                    // hi world
g();                        // hello world

f("hi", "universe");        // hi universe world
g("greetings");             // greetings hello world
g("greetings", "universe"); // greetings universe hello world
<script src="https://cdn.jsdelivr.net/npm/lodash@4.17.21/lodash.min.js"></script>

Thus the function constructed by _.partialRight is susceptible to the same problem that passing parseInt as callback has – more arguments can be passed in and will be passed in, since the callback for _.map always passes the element, the index, and the array. Therefore even though _.partialRight(_.groupBy, 'id') should have the second argument set to 'id', when _.map calls the function as callback(item, index, array) it turns into the fourth argument. And effectively the callback that gets executed is

(item, index, array) => _.groupBy(item, index, array, 'id')

This is why clamping down the arity with _.ary(fn, 1) or directly with _.unary() works – the extra arguments from _.map() would be discarded in that case and only the first one would be processed:

function print(a, b) {
  console.log(a, b);
}

const f = _.unary(_.partialRight(print, "world"));

f("hi");                    // hi world
f("hi", "universe");        // hi world
<script src="https://cdn.jsdelivr.net/npm/lodash@4.17.21/lodash.min.js"></script>

For the record, if you prefer more functional style and point-free style then you can use the Lodash FP distribution of Lodash which makes this easier. All the exported functions are curried and the arguments are changed so data is always last. Which allows you to more easily construct the processing for given data:

const obj = {a: [{ id: 1 }, {id: 1}, {id: 2}, {id: 3}], b: [{ id: 4 }, {id: 5}, {id: 5}, {id: 6}] };

const process = _.map(_.groupBy("id"));

console.log(process(obj));
.as-console-wrapper { max-height: 100% !important; }
<script src="https://cdn.jsdelivr.net/g/lodash@4(lodash.min.js+lodash.fp.min.js)"></script>
User contributions licensed under: CC BY-SA
10 People found this is helpful
Advertisement