Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

How to re-write _.every/_.all from Underscore.js using _.reduce (and _.each)

I'm working on re-writing the underlying code for many of the standard Underscore.js functions to work on my JavaScript skills and am a bit stuck with the _.every/_.all. It appears that in the library itself, the _.every/_.all function is written only using the existing _.each function, but I am being encouraged to write a version using my version of _.reduce (which already incorporates my version of _.each). I've provided the code for both functions below.

The first test my _.every function (see below as well) fails is one where all false values are passed in using the _.identity function (simply return value entered as argument) as the iterator:

Test:

  it('fails for a collection of all-falsy results', function() {
    expect(_.every([null, 0, undefined], _.identity)).to.equal(false);
  });

I have a few questions as to why my _.every function is failing the test shown above, along with multiple other tests (e.g.; mixed true/false values, undefined values, etc.):

-When calling the iterator function, do I need to use iterator.call or iterator.apply? If so, which do I use and how do I specify arguments?

-What benefit is there to using _.reduce here rather than just _.each, especially when the Underscore.js library does not use _.reduce?

-Why does return need to be called twice, once when calling the _.reduce function, and once within the anonymous function defined within _.reduce (I've also wondered this when building functions that utilize the _.map function)? To me, it seems like I am returning the result of the _.reduce function, which already is returning something.

_.every:

  _.every = function(collection, iterator) {
    // TIP: Try re-using reduce() here.
    return _.reduce(collection, function(allFound, item) {
      return iterator(item) && allFound;
    }, true);
  };

_.each:

_.each = function(collection, iterator) {
  // define spec for arrays
  if (Array.isArray(collection)) {
    for(var i = 0; i < collection.length; i++) {
      iterator(collection[i], i, collection);
    }
  }

  // define spec for objects
  else {
    for(var key in collection) {
      iterator(collection[key], key, collection);
    }
  }
};

_.reduce:

  _.reduce = function(collection, iterator, accumulator) {

    // add condition to set accumulator if no explicit starting value is given.
    if (arguments.length < 3) {
      accumulator = collection[0];
    }

    _.each(collection, function(value) {
      accumulator = iterator(accumulator, value);
    });

    return accumulator;
  };
like image 310
AvocadoRivalry Avatar asked Sep 30 '22 13:09

AvocadoRivalry


1 Answers

Your test isn't passing because it's not returning false as expected (though it is returning a falsey value).

_.every = function(collection, iterator) {
    return _.reduce(collection, function(allFound, item) {
        return iterator(item) && allFound;
    }, true);
};

What happens when you return iterator(item) && allFound is that if iterator(item) is falsey (but not false), it will not return false, but the value of iterator(item). To verify this for yourself, open a REPL, and type undefined && true; the result will be undefined, not false.

So if you want this to explicitly return false, and not just a falsey value, you'll have to coerce it to a boolean. You can do either Boolean(truthy_or_falsey_value) or !!truthy_or_falsey_value. I usually prefer the latter, so change your implementation thusly:

_.every = function(collection, iterator) {
    return _.reduce(collection, function(allFound, item) {
        return !!iterator(item) && allFound;
    }, true);
};

Your other questions:

When calling the iterator function, do I need to use iterator.call or iterator.apply? If so, which do I use and how do I specify arguments?

It depends on what your goal is. call and apply are primarily used when you want to control the value of the this keyword in the function body. Some of JavaScript's built-in array methods (like Array.prototype.map and Array.prototype.filter) take a thisArg, which is what is supplied do the callback using call or apply. As far as the difference between call and apply, it's only how the arguments are handled. See this answer for more details.

What benefit is there to using reduce here rather than just each, especially when the Underscore.js library does not use reduce?

Probably none, or very little. There could be a performance difference, but the best way to find out would be to profile both approaches.

Why does return need to be called twice, once when calling the _.reduce function, and once within the anonymous function defined within _.reduce

If you want a function -- any function -- to return a value, you must call return from within that function. You can't expect to call return from an inner function, and expect the enclosing function to magically understand that it's supposed to, in turn, return the value of the called function. Some languages default to returning the value of the last expression in the function if return isn't explicitly called, which is either convenient or confusing, depending on your perspective. If you have experience with such a language (Ruby, for example), then all the return statements may seem a little excessive to you.

As an editorial note, I feel that iterator is a poor naming choice for the testing function. It is not actually iterating over anything (the function that it's an argument to is doing any iteration). A better name might be the very generic callback or cb. The term "predicate" means a function that maps a value to true or false, which is my preferred terminology. Another common choice is simply test, since it is, after all, just a function that's performing a binary filter on its argument.

like image 67
Ethan Brown Avatar answered Oct 21 '22 09:10

Ethan Brown