Home > OS >  How can you reduce(...) the entries of a JavaScript Map object without expanding to a list?
How can you reduce(...) the entries of a JavaScript Map object without expanding to a list?

Time:04-20

It seems there's no good standard library way of doing something like this?

let thismap = new Map([[1,2],[2,3]])
console.log(thismap.entries().reduce((prev, [a,b])=>prev   a * b, 0))

Uncaught TypeError: thismap.entries(...).reduce is not a function

I assume this is due to the entries() function returning an iterator? I don't want to Array.from(thismap.entries()).reduce(...), as that would unnecessarily build the array in memory. It feels like I'm missing something, but I also don't want to reimplement something that should be in the standard library.

I suppose if I was using an object instead (not a satisfactory solution here for other reasons), the entries() would essentially be an array expansion instead of an iterator (although I suppose it could be implemented with memory efficiency in mind). But still, I'd want to know how to reduce an iterator

CodePudding user response:

You can use a for of loop and handle the summing manually. This uses the iterator without creating a temporary array. Note that here we don't even have to call entries manually because Map.prototype[Symbol.iterator] === Map.prototype.entries.

const map = new Map([[1, 2], [2, 3]])

let sum = 0
for (const [a, b] of map) sum  = a * b

console.log(sum)

You can of course also factor this out into a utility function in case you need it more often. Here I created a function lazyReduce that works like Array.prototype.reduce but operates on iterables of any kind:

function lazyReduce (originalIterable, callback, initialValue) {
  let i = 0
  let accumulator = initialValue
  let iterable = originalIterable
  
  // This part exists to implement the behavior of reduce without initial value
  // in the same way Array.prototype.reduce does it
  if (arguments.length < 3) {
    iterable = iterable[Symbol.iterator]()
    const { value: accumulator, done } = iterable.next()
    if (done) throw new TypeError('Reduce of empty iterable with no initial value')
    i  
  }
  
  for (const element of iterable) {
    accumulator = callback(accumulator, element, i  , originalIterable)
  }
  
  return accumulator
}

const map = new Map([[1, 2], [2, 3]])

console.log(lazyReduce(map, (prev, [a, b]) => prev   a * b, 0))

If you wanted, you could extend the prototypes of Map, Set, etc., i.e. Map.prototype.reduce = function (...args) { return lazyReduce(this, ...args) }. (Note: Some other things that return iterators would be harder to extend, but still possible. For example RegExpStringIterator which doesn't exist as global variable, but you can still do Object.getPrototypeOf(''.matchAll(/./g)).reduce = .... Similar ideas would work for Generator.)

CodePudding user response:

I also don't want to reimplement something that should be in the standard library.

It really should indeed. There's a proposal to add it: Iterator Helpers. While waiting for it, you can already use the polyfill which will make your original code work :-)

  • Related