2

The code below is a reimplementation of the _.reduce() method. It's not mine, but I am using it to get to grips with how _.reduce works. It is currently failing on two tests:

  1. should be able to reduce a collection to a single value - AssertionError: expected 'aabcdabcdabcdabcd' to equal 'abcd'
  2. should support initial state AssertionError: expected 'initabcdabcdabcdabcd' to equal 'initabcd'
_.reduce = function (list, iteratee, memo, context) {
  if (context) iteratee = iteratee.bind(context);
  _.each(list, (elem, index) => {
    if (memo === undefined) {
      memo = elem;
      memo =iteratee(memo, elem, index, list);
    } else memo = iteratee(memo, elem, index, list);
  });
  return memo;
};

I can't understand why this is happening. It looks to me as if this should run as expected. Can anyone provide further information?

UPDATE I was able to solve the 2nd error thanks to @georg spotting the issues with my _.each() function. The first error remains, but is slightly different:

  1. should be able to reduce a collection to a single value AssertionError: expected 'aabcd' to equal 'abcd'

This is the test code that relates to the error messages

var mocks = {
  arr: ['a','b','c','d'], // >= 4 elements, all should be truthy
  obj: {a:1,b:2,c:3,d:4}, // >= 4 values, all should be truthy
  halfTruthyArr: [null,'b',null,'d'], // >= 4 elements, half should be falsy
  halfTruthyObj: {a:1,b:null,c:3,d:null}, // >= 4 values, half should be falsy
  string: 'This is a string.',
  reverseString: function (string) {
    if (typeof string === 'string') return string.split('').reverse().join('');
  }
};

   describe('reduce', function () {

    afterEach(function () {
      called = false;
    });

    it('should be able to reduce a collection to a single value', function () {
      _.reduce(mocks.arr, function (accumulator, el, i, arr) {
        arr[i].should.equal(el);
        return accumulator.toString() + el.toString();
      }).should.equal(mocks.stringifiedArrElms);
    });
jahe
  • 41
  • 3
  • What is the code that runs that expects `'abcd'` or `'initabcd'`? – VLAZ May 28 '21 at 10:10
  • Would be way easier to answer/debug, if you provide it in a runnable form. – Thomas Deniffel May 28 '21 at 10:12
  • @ThomasDeniffel I'm a real newbie to this so I'm unsure what you mean by "runnable form". I tried to include the test files but they're too long to comment. Can you clarify what you mean please? – jahe May 28 '21 at 10:28

2 Answers2

1

This is wrong:

  memo = elem[0];
  memo =iteratee(memo, elem, index, list);

should be

  memo = elem

Basically, when no init value is given, you should take the first element as the current value and do not invoke the callback on it.

_ = {}

_.each = (a, fn) => a.forEach(fn)

_.reduce = function (list, iteratee, memo, context) {
    if (context) iteratee = iteratee.bind(context);
    _.each(list, (elem, index) => {
        if (memo === undefined)
            memo = elem;
        else
            memo = iteratee(memo, elem, index, list);
    });
    return memo;
};


console.log(_.reduce([1], (a, x) => a + '|' + x))
console.log(_.reduce([1, 2, 3], (a, x) => a + '|' + x))

console.log(_.reduce([1], (a, x) => a + '|' + x, '@'))
console.log(_.reduce([1, 2, 3], (a, x) => a + '|' + x, '@'))

Of course, memo === undefined is quite naive (nothing stops the callback from returning undefined somewhere in the middle), a safer option would be

let noMemo = Symbol();
if (arguments.length < 3)
    memo = noMemo;

_.each(list, (elem, index) => {
    if (memo === noMemo)
        memo = elem;
    else...
georg
  • 211,518
  • 52
  • 313
  • 390
  • Thanks for your answer. Unfortunately even with your corrections the same tests are still failing, the errors are slightly different though - 1. should be able to reduce a collection to a single value‣ AssertionError: expected 'aabcdabcdabcdabcd' to equal 'abcd' 2.should support initial state‣ AssertionError: expected 'initabcdabcdabcdabcd' to equal 'initabcd' – jahe May 28 '21 at 10:34
  • @jahe: share the source of `_.each` – georg May 28 '21 at 10:44
  • ` _.each = function (collection, iteratee, context) { if (Array.isArray(collection)) { for (let key of collection) { for (let i = 0; i < collection.length; i++){ iteratee.call(context,collection[i], i, collection); } } } else { for (let prop in collection) { if (collection.hasOwnProperty(prop)) { iteratee.call(context,collection[prop],prop,collection); } } } return collection }; ` – jahe May 28 '21 at 10:45
  • @jahe: well, this is wrong either. You're iterating an array twice - note the two nested `for` statements. – georg May 28 '21 at 10:46
  • so do you think the best solution is to not use `_.each()` in _.reduce() at all? I'd like to avoid changing the `_.each()` function as it is currently passing all the tests – jahe May 28 '21 at 10:49
  • @jahe: your `each` is broken (try `_.each([1,2,3,4], x => console.log(x))`), so you have to fix it anyways. – georg May 28 '21 at 10:52
  • I take back my previous comment! You're a genius! I took out the first for loop in the `_.each()` method and it still works AND the second error message is gone. My only issue now is this: should be able to reduce a collection to a single value‣ AssertionError: expected 'aabcd' to equal 'abcd' – jahe May 28 '21 at 10:56
0

georg already pointed this out in his answer, but I think I can make it a bit more explicit: you are using the first element twice. The following snippet of your code is meant to use the first element of the list when no initial accumulator is provided:

    if (memo === undefined) {
      memo = elem;
      memo =iteratee(memo, elem, index, list);
    }

We can substitute elem for memo in the assignments to see how the first element ends up being used twice:

    if (memo === undefined) {
      memo =iteratee(elem, elem, index, list);
    }

The solution is to simply not invoke iteratee on the first element, as georg already hinted:

    if (memo === undefined) {
      memo = elem;
    }

georg also suggested using a unique Symbol() to more reliably indicate the first element, rather than undefined which might also be a return value of your iteratee halfway through the collection. An alternative approach would be to simply use a boolean state variable (below named first):

_.reduce = function (list, iteratee, memo, context) {
  var first = false;
  if (arguments.length < 3) {
    first = true;
  } else if (context) {
    iteratee = iteratee.bind(context);
  }
  _.each(list, (elem, index) => {
    if (first) {
      memo = elem;
      first = false;
    } else memo = iteratee(memo, elem, index, list);
  });
  return memo;
};
Julian
  • 4,176
  • 19
  • 40