Lodash reduce with includes or orderBy iteratee use cases

Viewed 278

From the lodash documentation for reduce:

_.reduce(collection, [iteratee=_.identity], [accumulator])

Reduces collection to a value which is the accumulated result of running each element in collection thru iteratee, where each successive invocation is supplied the return value of the previous. If accumulator is not given, the first element of collection is used as the initial value. The iteratee is invoked with four arguments: (accumulator, value, index|key, collection).

Many lodash methods are guarded to work as iteratees for methods like _.reduce, _.reduceRight, and _.transform.

The guarded methods are: assign, defaults, defaultsDeep, includes, merge, orderBy, and sortBy

I can see how reduce can be applied to most of the other methods, since they roughly follow the method signature. E.g., assign and defaults are of the form (in informal TypeScript-ish pseudocode):

_.assign and _.defaults: (object: object, sources: collection) => object
                          ^ accumulator   ^ value                 ^ same type as accumulator

Similarly, sortBy also makes sense:

_.sortBy: (collection: object, iteratees: sort_fn[]) => object
           ^ accumulator       ^ value                  ^ same type as accumulator

I can see useful use cases for these, e.g.:

console.log('reduce',
    _.reduce([{a: 2, b: 4}, [123, 52], {a: 5}], _.assign),
    _.reduce([{a: 2, b: 4}, [123, 52], {a: 5}], _.defaults),
    _.reduce([[e => e.a], [e => e.b]], _.sortBy,
      [{a: 3, b: 2}, {a: 3, b: 3}, {a: 2, b: 15}, {a: -2, b: 3}])
);
<script src="https://cdnjs.cloudflare.com/ajax/libs/lodash.js/4.17.15/lodash.min.js"></script>


However, I'm not sure how this would be used with orderBy and includes. I.e.,

_.orderBy: (collection: object, iteratees: sort_fn[], orders: ('asc' | 'desc')[]) => object
            ^ accumulator       ^ value               ^ index???                     ^ same type as accumulator

Looking at the source and following some of the method calls, it seems that this still works, despite the third parameter being the array of 'asc' | 'desc' values (because of the guarding, it treats it specially). Ultimately, with the guarding it seems that it defaults all of the orders to asc, so it acts just like sortBy.

console.log('reduce',
    _.reduce([[e => e.a], [e => e.b]], _.orderBy,
      [{a: 3, b: 2}, {a: 3, b: 3}, {a: 2, b: 15}, {a: -2, b: 3}])
);
<script src="https://cdnjs.cloudflare.com/ajax/libs/lodash.js/4.17.15/lodash.min.js"></script>

Thus it seems that, if used this way, the guarding is not very useful, and just makes this an alterative to sortBy, effectively rendering the orders parameter useless.

For includes, I'm not even sure how this can be used as a reducer function, because its return type (a boolean) is not the same as its first parameter, e.g.:

_.includes: (collection: object, value: any) => boolean
             ^ accumulator       ^ value        ^ not same type as accumulator?

My question is: Is there any way to (usefully) use _.orderBy and _.includes as the _.reduce or _.transform iteratee? And if not, why does the lodash documentation list it there as being a guarded function that can be used this way?

Sorry if this sounds a bit like two questions, but I figured that they were related enough that they belonged together.

1 Answers

TL;DR (see below for the full story)

My guess is that in the case of _.includes, it was never meant to be guarded for use with _.reduce, and instead that was either a slip-up from years ago that is still around or an unaddressed need to better clarify that part of the docs.

Instead, the intention when they added the guard parameter was to make it usable with _.every, _.some and the likes when partially applied.

Like this:

_.every([1, 2], _.partial(_.includes, [1, 2, 3, 4])) // => true
_.every([1, 5], _.partial(_.includes, [1, 2, 3, 4])) // => false

Full story

I was trying to replace a specific part of my code that was made with some ugly _.filters, _.clones and !_.includes with something more elegant.

I then tried, as I usually do in these cases, to scan the lodash docs and try to ramp up the creative juices in my brain.

Then I found that same line in the documentation that you mention, and, after playing a bit with _.reduce and _.includes, reached the same conclusion as you. That it didn't make sense.

Thinking that I was probably wrong, I googled about it hoping that I could find some clever examples and ended up here.

It was good to see that I was not crazy, or at least not crazy alone, in this world, but it still didn't scratch the itch I had in my brain since there was no answer posted to this question (as of the time I'm writing this).

So I went for the last place left where I could try and dig some info about that: the source code.

After a while going back blame by blame, I finally found the commit that introduced that change.

The most interesting part of it was a test that was added to the _.includes section of the tests:

      strictEqual(_.includes([-0], 0), true);
    });

+   test('should work as an iteratee for methods like `_.reduce`', 1, function() {
+     var array1 = [1, 2, 3],
+         array2 = [2, 3, 1];
+
+     ok(_.every(array1, _.partial(_.includes, array2)));
+   });
+
    test('should be aliased', 2, function() {
      strictEqual(_.contains, _.includes);
      strictEqual(_.include, _.includes);

Look close, though, and you'll see that it doesn't test anything related to _.reduce at all!

So next stop, I went to the test suite and tried to find any extra test related to that functionality that could've been added since that commit, and, interestingly enough, I found quite the opposite!

Apparently, one day there was a large change across the entire test suite to replace everywhere where it used lodash functions not related to the actual tested function to use them from a second, older (and stable) instance of lodash, probably to avoid a bug that could make the test not catch the bug that could make the test not catch the bug that could...

In that change, someone was forced to actually pay attention to every bit of _ in the test suite and probably realized that the description of that specific test didn't make sense at all.

And so they changed it:

       assert.strictEqual(_.includes([0], -0), true);
     });

-    QUnit.test('should work as an iteratee for methods like `_.reduce`', function(assert) {
+    QUnit.test('should work as an iteratee for methods like `_.every`', function(assert) {
       assert.expect(1);

       var array1 = [1, 2, 3],
           array2 = [2, 3, 1];

-      assert.ok(_.every(array1, _.partial(_.includes, array2)));
+      assert.ok(lodashStable.every(array1, lodashStable.partial(_.includes, array2)));
     });
   }(1, 2, 3, 4));

Last, but not least, I tried reading the source code of the _.every function as of the commit before the one that added the guard in the _.includes function, but that didn't gave me the quick answer I was expecting, so I got the version 3.5.0 (which was the last one released without that change in the _.includes function) from a CDN, included it directly in a <script> tag in an empty page and quickly tested basically the same line that was added in that test suite.

And voilĂ , it didn't work.

So I concluded that the original intention was probably to make that case (with the _.every function) work, but in the docs, they accidentally added it to the list of guarded functions to use with the _.reduce function instead. Or else they just relied on the "methods like" bit of the phrase and considered it ok enough to just put it in there since it wasn't technically tying it specifically to the _.reduce function.

Either way I opened an issue here in the lodash repo to try to bring attention to that.

Related