Question

Working through Eloquent JavaScript and High Order Functions - section in Functional Programming.

Trying to define a reduce function and use it in a higher order function countWords();, which takes a given array and counts number of times each particular value is present and puts it in an object.

I.e. this works:

function combine(countMap, word) {
    countMap[word] = ++countMap[word] || 1; // made the edit

    return countMap;
}

function countWords(wordArray) {
    return wordArray.reduce(combine, {});
}

var inputWords = ['Apple', 'Banana', 'Apple', 'Pear', 'Pear', 'Pear'];

countWords(inputWords); // {Apple: 2, Banana: 1, Pear: 3}

I.e. this does not:

function combine(countMap, word) {
    countMap[word] = ++countMap[word] || 1;

    return countMap;
}

function forEach(array, action) {
    for (var i = 0; i < array.length; i++) {
        action(array[i]);
    }
}

function reduce(fn, base, array) {
    forEach(array, function (element) {
        base = fn(base, element);
    });

    return base;
}

function countWords(wordArray) {
    return reduce(combine, {}, wordArray);
}

var inputWords = ['Apple', 'Banana', 'Apple', 'Pear', 'Pear', 'Pear'];

countWords(inputWords); //    returned this - [object Object] { ... }  - this is no longer an issue after fix keeping it noted for reference to the original issue.

Any help on this would be great. Thanks.

Was it helpful?

Solution 3

The reason is that your ForEach implementation is wrong. you should set i = 0;

function forEach(array, action) {
    for(var i = 0; i < array.length; i++) {
        action(array[i]);
    }   
}

There seems to be something wrong. You update an object. ++countMap

function combine(countMap, word) {
    countMap[word] = ++countMap || 1;
    return countMap; 
}

It should be

function combine(countMap, word) {
    countMap[word] = ++countMap[word] || 1;
    return countMap; 
}

I add a jsbin here

OTHER TIPS

Your original reduce is actually broken, despite you saying that it works.

Here's a reduce that actually functions

var words = ["foo", "bar", "hello", "world", "foo", "bar"];

var wordIndexer = function(map, word) { 
  map[word] = map[word] || 0;
  map[word]++;
  return map;
};

var count = word.reduce(wordIndexer, {});

console.log(count);

// Object {foo: 2, bar: 2, hello: 1, world: 1}

That said, I'm not entirely sure what you're trying to do with the second half of your post. Are you just trying to write implementations for forEach and reduce so you can understand how they work?


I would write the forEach like this

var forEach = function(arr, callback) {
  for (var i=0, len=arr.length; i<len; i++) {
    callback(arr[i], i, arr);
  }
  return arr;
};

And reduce like this

var reduce = function(arr, callback, initialValue) {
  var result = initialValue;
  forEach(arr, function(elem, idx) {
    result = callback(result, elem, idx, arr);
  });
  return result;
};

Test them out

var numbers = [10, 20, 30];

forEach(numbers, function(num, idx) {
  console.log(idx, num);
});

// 0, 10
// 1, 20
// 2, 30
//=> [10, 20, 30]

var n = reduce(numbers, function(sum, num, idx, arr) {
  return sum = sum + num;
}, 0);

console.log(n);
//=> 60

For those curious about reduce callback, I matched the native .reduce callback

I guess it depends on if you want forEach and reduce to be similar (simple) or as close as possible/reasonable to the ECMA5 spec (ignoring browser bugs), I like close as possible/reasonable.

Array.prototype.forEach ( callbackfn [ , thisArg ] )

callbackfn should be a function that accepts three arguments. forEach calls callbackfn once for each element present in the array, in ascending order. callbackfn is called only for elements of the array which actually exist; it is not called for missing elements of the array.

If a thisArg parameter is provided, it will be used as the this value for each invocation of callbackfn. If it is not provided, undefined is used instead.

callbackfn is called with three arguments: the value of the element, the index of the element, and the object being traversed.

forEach does not directly mutate the object on which it is called but the object may be mutated by the calls to callbackfn.

The range of elements processed by forEach is set before the first call to callbackfn. Elements which are appended to the array after the call to forEach begins will not be visited by callbackfn. If existing elements of the array are changed, their value as passed to callback will be the value at the time forEach visits them; elements that are deleted after the call to forEach begins and before being visited are not visited.

When the forEach method is called with one or two arguments, the following steps are taken:

  1. Let O be the result of calling ToObject passing the this value as the argument.
  2. Let lenValue be the result of calling the [[Get]] internal method of O with the argument "length".
  3. Let len be ToUint32(lenValue).
  4. If IsCallable(callbackfn) is false, throw a TypeError exception.
  5. If thisArg was supplied, let T be thisArg; else let T be undefined.
  6. Let k be 0.
  7. Repeat, while k < len
  8. Let Pk be ToString(k).
  9. Let kPresent be the result of calling the [[HasProperty]] internal method of O with argument Pk.
  10. If kPresent is true, then
  11. Let kValue be the result of calling the [[Get]] internal method of O with argument Pk.
  12. Call the [[Call]] internal method of callbackfn with T as the this value and argument list containing kValue, k, and O.
  13. Increase k by 1.
  14. Return undefined.

The length property of the forEach method is 1.

NOTE The forEach function is intentionally generic; it does not require that its this value be an Array object. Therefore it can be transferred to other kinds of objects for use as a method. Whether the forEach function can be applied successfully to a host object is implementation-dependent.

-

Array.prototype.reduce ( callbackfn [ , initialValue ] )

callbackfn should be a function that takes four arguments. reduce calls the callback, as a function, once for each element present in the array, in ascending order.

callbackfn is called with four arguments: the previousValue (or value from the previous call to callbackfn), the currentValue (value of the current element), the currentIndex, and the object being traversed. The first time that callback is called, the previousValue and currentValue can be one of two values. If an initialValue was provided in the call to reduce, then previousValue will be equal to initialValue and currentValue will be equal to the first value in the array. If no initialValue was provided, then previousValue will be equal to the first value in the array and currentValue will be equal to the second. It is a TypeError if the array contains no elements and initialValue is not provided.

reduce does not directly mutate the object on which it is called but the object may be mutated by the calls to callbackfn.

The range of elements processed by reduce is set before the first call to callbackfn. Elements that are appended to the array after the call to reduce begins will not be visited by callbackfn. If existing elements of the array are changed, their value as passed to callbackfn will be the value at the time reduce visits them; elements that are deleted after the call to reduce begins and before being visited are not visited.

When the reduce method is called with one or two arguments, the following steps are taken:

  1. Let O be the result of calling ToObject passing the this value as the argument.
  2. Let lenValue be the result of calling the [[Get]] internal method of O with the argument "length".
  3. Let len be ToUint32(lenValue).
  4. If IsCallable(callbackfn) is false, throw a TypeError exception.
  5. If len is 0 and initialValue is not present, throw a TypeError exception.
  6. Let k be 0.
  7. If initialValue is present, then
  8. Set accumulator to initialValue.
  9. Else, initialValue is not present
  10. Let kPresent be false.
  11. Repeat, while kPresent is false and k < len
  12. Let Pk be ToString(k).
  13. Let kPresent be the result of calling the [[HasProperty]] internal method of O with argument Pk.
  14. If kPresent is true, then
  15. Let accumulator be the result of calling the [[Get]] internal method of O with argument Pk.
  16. Increase k by 1.
  17. If kPresent is false, throw a TypeError exception.
  18. Repeat, while k < len
  19. Let Pk be ToString(k).
  20. Let kPresent be the result of calling the [[HasProperty]] internal method of O with argument Pk.
  21. If kPresent is true, then
  22. Let kValue be the result of calling the [[Get]] internal method of O with argument Pk.
  23. Let accumulator be the result of calling the [[Call]] internal method of callbackfn with undefined as the this value and argument list containing accumulator, kValue, k, and O.
  24. Increase k by 1.
  25. Return accumulator.

The length property of the reduce method is 1.

NOTE The reduce function is intentionally generic; it does not require that its this value be an Array object. Therefore it can be transferred to other kinds of objects for use as a method. Whether the reduce function can be applied successfully to a host object is implementation-dependent.

Which for me I would write (and these are not 100% to spec, but close) and keep in my personal library.

function firstToCapital(inputString) {
    return inputString.charAt(0).toUpperCase() + inputString.slice(1).toLowerCase();
}

function isClass(inputArg, className) {
    return Object.prototype.toString.call(inputArg) === '[object ' + firstToCapital(className) + ']';
}

function checkObjectCoercible(inputArg) {
    if (typeof inputArg === 'undefined' || inputArg === null) {
        throw new TypeError('Cannot convert argument to object');
    }

    return inputArg;
};

function ToObject(inputArg) {
    checkObjectCoercible(inputArg);
    if (isClass(inputArg, 'boolean')) {
        inputArg = new Boolean(inputArg);
    } else if (isClass(inputArg, 'number')) {
        inputArg = new Number(inputArg);
    } else if (isClass(inputArg, 'string')) {
        inputArg = new String(inputArg);
    }

    return inputArg;
}

function ToUint32(inputArg) {
    return inputArg >>> 0;
}

function throwIfNotAFunction(inputArg) {
    if (!isClass(inputArg, 'function')) {
        throw TypeError('Argument is not a function');
    }

    return inputArg;
}

function forEach(array, fn, thisArg) {
    var object = ToObject(array),
        length,
        index;

    throwIfNotAFunction(fn);
    length = ToUint32(object.length);
    for (index = 0; index < length; index += 1) {
        if (index in object) {
            fn.call(thisArg, object[index], index, object);
        }
    }
}

function reduce(array, fn, initialValue) {
    var object = ToObject(array),
        accumulator,
        length,
        kPresent,
        index;

    throwIfNotAFunction(fn);
    length = ToUint32(object.length);
    if (!length && arguments.length === 2) {
        throw new TypeError('reduce of empty array with no initial value');
    }

    index = 0;
    if (arguments.length > 2) {
        accumulator = initialValue;
    } else {
        kPresent = false;
        while (!kPresent && index < length) {
            kPresent = index in object;
            if (kPresent) {
                accumulator = object[index];
                index += 1;
            }
        }

        if (!kPresent) {
            throw new TypeError('reduce of empty array with no initial value');
        }
    }

    while (index < length) {
        if (index in object) {
            accumulator = fn.call(undefined, accumulator, object[index], index, object);
        }

        index += 1;
    }

    return accumulator;
}

function keys(object) {
    if (!isClass(object, 'object') && !isClass(object, 'function')) {
        throw new TypeError('Argument must be an object or function');
    }

    var props = [],
        prop;

    for (prop in object) {
        if (object.hasOwnProperty(prop)) {
           props.push(prop);
        }
    }

    return props;
}

var inputWords = ['Apple', 'Banana', 'Apple', 'Pear', 'Pear', 'Pear'];

var counts = reduce(inputWords, function (previous, element) {
    previous[element] = ++previous[element] || 1;

    return previous;
}, {});

forEach(keys(counts), function (key) {
    console.log(key, this[key]);
}, counts);

On jsFiddle

Of course this may be a little OTT for what you are doing. :)

Licensed under: CC-BY-SA with attribution
Not affiliated with StackOverflow
scroll top