251

Is there any way to map/reduce/filter/etc a Set in JavaScript or will I have to write my own?

Here's some sensible Set.prototype extensions

Set.prototype.map = function map(f) {
  var newSet = new Set();
  for (var v of this.values()) newSet.add(f(v));
  return newSet;
};

Set.prototype.reduce = function(f,initial) {
  var result = initial;
  for (var v of this) result = f(result, v);
  return result;
};

Set.prototype.filter = function filter(f) {
  var newSet = new Set();
  for (var v of this) if(f(v)) newSet.add(v);
  return newSet;
};

Set.prototype.every = function every(f) {
  for (var v of this) if (!f(v)) return false;
  return true;
};

Set.prototype.some = function some(f) {
  for (var v of this) if (f(v)) return true;
  return false;
};

Let's take a little set

let s = new Set([1,2,3,4]);

And some stupid little functions

const times10 = x => x * 10;
const add = (x,y) => x + y;
const even = x => x % 2 === 0;

And see how they work

s.map(times10);    //=> Set {10,20,30,40}
s.reduce(add, 0);  //=> 10
s.filter(even);    //=> Set {2,4}
s.every(even);     //=> false
s.some(even);      //=> true

Isn't that nice ? Yeah, I think so too. Compare that to the ugly iterator usage

// puke
let newSet = new Set();
for (let v in s) {
  newSet.add(times10(v));
}

And

// barf
let sum = 0;
for (let v in s) {
  sum = sum + v;
}

Is there any better way to accomplish map and reduce using a Set in JavaScript?

Levi Roberts
  • 1,277
  • 3
  • 22
  • 44
Mulan
  • 129,518
  • 31
  • 228
  • 259
  • The problem with map-reduce-ing a `Set` is that Sets aren't Functors. – Bartek Banachewicz Oct 20 '15 at 10:53
  • 5
    Well, consider `var s = new Set([1,2,3,4]); s.map((a) => 42);`. It changes the number of elements, which `map` typically isn't supposed to do. Even worse if you're only comparing parts of the kept objects, because then technically it's unspecified which one you'll get. – Bartek Banachewicz Oct 20 '15 at 10:55
  • I had considered that, but I'm not sure I (personally) would consider that invalid. OK so at least `forEach` exists for that scenario, but why no `reduce` then ? – Mulan Oct 20 '15 at 10:58
  • I'd say that is an oversight. It's perfectly fine to fold (reduce) a Set. – Bartek Banachewicz Oct 20 '15 at 10:59
  • 4
    Some related reading: https://esdiscuss.org/topic/set-some-every-reduce-filter-map-methods – CodingIntrigue Oct 20 '15 at 11:01
  • `every` and `some` should not be implemented using `filter`, so that they can `return` early – Bergi Oct 20 '15 at 15:20
  • @Bergi thanks, I updated them :) – Mulan Oct 20 '15 at 18:51
  • @naomik, Stay compatible with the Array method: `Set.prototype.reduce = function(f,acc) { var started = arguments.length > 1; for(var v of this) acc = started? f(acc,v): (started=true,v); if(!started) throw new TypeError("reduce of empty Set with no initial value"); return acc; }` – Thomas Sep 04 '16 at 12:38
  • @BartekBanachewicz All these iterable methods should be supported against an `Iterable` interface, and `Set.map` should produce some arbitrary iterable object, which will have the same number of elements. When sensible for other methods, the produced iterable should be a Set. This is common sense in libraries for any statically typed language you look at, but dynamic languages like JS always seem to f*ck this up. – Asad Saeeduddin Dec 29 '16 at 16:29
  • .map isn't supposed to be just an arbitrary word though: it's supposed to be a guarantee of Functor behavior. There are laws behind Functors that allow you to generalize the behavior of .map to different sorts of containers. By creating a native type with a .map that violates those laws, and especially _calling_ it .map, hurts the ability to abstract and thus hurts the utility of the language (adds even more "WTF Javascript" gotchas!). – Dtipson Feb 08 '17 at 17:10
  • It's not very "sensible" to extend core JS types in most situations. – andrezsanchez Apr 25 '18 at 00:09
  • @Mulan "but why no reduce then ?" because you would have to guarantee that the insertion order of the elements is the order you want to traverse the set. This order matters because not all callbacks fed to `.reduce` will generate the same output with different orders (string concatenation, matrix multiplication, etc...). Plus it seems odd to rely on the order of insertion of the elements in the set. Classically, there *is* no order. The most efficient implementations of Set just happen to give it to us for free. (I'm sure there are good specific uses for it though!) – Pharoah Jardin Aug 06 '22 at 00:36

5 Answers5

229

A short-hand way to do it is to convert it to an array via the ES6 spread operator.

Then all the array functions are available to you.

const mySet = new Set([1,2,3,4]);
[...mySet].reduce(...);
Audwin Oyong
  • 2,247
  • 3
  • 15
  • 32
ZephDavies
  • 3,964
  • 2
  • 14
  • 19
  • 1
    Because the functions are not available for Set! This is a complete, guided and understood workaround that is not as yet present in this topic. The fact it 'takes longer' is a sad price to pay for a workaround until Set implements these features! – ZephDavies Mar 07 '17 at 15:37
  • 1
    What's the difference between this and Array.from – pete Jun 21 '17 at 04:24
  • 31
    For me at least, the difference between this and `Array.from` is that `Array.from` works with TypeScript. Using `[...mySet]` gives the error: `TS2461: Type 'Set' is not an array type.` – Mikal Madsen Jul 14 '17 at 13:18
  • 2
    For spread vs Array.from(), see https://stackoverflow.com/a/40549565/5516454 Basically, both are usable here. Array.from() can additionally do array-like objects which do not implement the `@@iterator` method. – ZephDavies Jan 24 '18 at 11:23
  • still doesn't work for me with typescript. I get `ERROR TypeError: this.sausages.slice is not a function` – Simon_Weaver Jan 04 '19 at 05:21
  • this.sausages is a Set, which does not have the .slice() method. You need to convert it to an array, using Array.from(). I.E: `this.sausages = Array.from(this.sausages)` – ZephDavies Jan 05 '19 at 10:53
  • 3
    In V8 at least, another difference is that `[...mySet]` will fail with stack overflow for large Sets. `Array.from()` does not use stack memory per element, so it is less risky when the number of elements may be large. – peterflynn May 25 '22 at 21:20
30

To sum up the discussion from comments: while there are no technical reasons for set to not have reduce, it's not currently provided and we can only hope it changes in ES7.

As for map, calling it alone could violate the Set constraint, so its presence here might be debatable.

Consider mapping with a function (a) => 42 - it will change the set's size to 1, and this might or might not be what you wanted.

If you're ok with violating that because e.g. you're going to fold anyway, you can apply the map part on every element just before passing them to reduce, thus accepting that the intermediate collection (which isn't a Set at this point) that's going to be reduced might have duplicated elements. This is essentially equivalent to converting to Array to do processing.

Bartek Banachewicz
  • 38,596
  • 7
  • 91
  • 135
  • 1
    This is mostly good, except (using the code above), `s.map(a => 42)` will result in `Set { 42 }` so the mapped result will be a different length but there will not be "duplicated" elements. Maybe update the wording and I'll accept this answer. – Mulan Oct 20 '15 at 11:19
  • @naomik Oh derp I was just finishing my first coffee when writing that. On the second look, the intermediate collection passed to reduce *might* have immediate elements if you accept it's not a set - that's I meant. – Bartek Banachewicz Oct 20 '15 at 11:27
  • 2
    Oh I get it - map has to map to the same type, hence possible collisions in the destination set. When I found this question I was thinking map would map to an array from a set. (as if you did set.toArray().map()` – Simon_Weaver Jan 04 '19 at 05:24
  • 2
    In Scala and Haskell, sets support a map operation - it can reduce the number of elements in the set. – Velizar Hristov Jan 21 '19 at 17:08
14

The cause of the lack of map/reduce/filter on Map/Set collections seem to be mainly conceptual concerns. Should each collection type in Javascript actually specify its own iterative methods only to allow this

const mySet = new Set([1,2,3]);
const myMap = new Map([[1,1],[2,2],[3,3]]);

mySet.map(x => x + 1);
myMap.map(([k, x]) => [k, x + 1]);

instead of

new Set(Array.from(mySet.values(), x => x + 1));
new Map(Array.from(myMap.entries(), ([k, x]) => [k, x + 1]));

An alternative were to specify map/reduce/filter as part of the iterable/iterator protocol, since entries/values/keys return Iterators. It is conceivable though that not every iterable is also "mappable". Another alternative were to specify a separate "collection protocol" for this very purpose.

However, I do not know the current discussion on this topic at ES.

  • 2
    _Should each collection type in Javascript actually specify its own iterative methods only to allow this?_ Yes. All that `Array.from` with `new Set` is a workaround and is much less readable than `myArray.filter(isBiggerThan6Predicate);` Now I have to write my own `filterSet` function, so I can write clean code: `filterSet(setWithNumbers, biggerThan6Predicate);` – KulaGGin Apr 07 '22 at 12:18
  • How is it about performance? I could imagine that new `Set(Array.from(mySet...))` has a lot of overhead compared to an own iterative method. – Cadoiz Mar 14 '23 at 07:33
0

Thought this was worth mentioning in terms of speed, if you are deciding between the Set Methods forEach() which invokes a callback for each element, or values() which returns an iterator with all the values in a Set.

As an example lets filer out the even numbers from a Set:

const generateSet = (n, m) => {
  // Generate list of length n with random numbers between 0 and m
  let arr = Array.from({ length: n }, () =>
    Math.floor(Math.random() * m)
  );
  // Convert to Set
  var set = new Set(arr);
  return set;
};

Our two filter functions:

const filterValues = (set) => {
  // Using Iterator
  const it = set.values();
  let result = it.next();
  while (!result.done) {
    if (result.value % 2 === 0) {
      set.delete(result.value);
    }
    result = it.next();
  }
};

const filterForEach = (set) => {
  // invokes a callback
  set.forEach((item) => {
    if (item % 2 === 0) {
      set.delete(item);
    }
  });
};

For timing we use Timing these on random array including numbers from range [0, 10,000,000] containing 5,000,000 items:

let setValues = generateSet(5000000, 10000000);
console.time("Filter with values()");
filterValues(setValues);
console.timeEnd("Filter with values()");

let setForEach = generateSet(5000000, 10000000);
console.time("Filter with forEach()");
filterForEach(setForEach);
console.timeEnd("Filter with forEach()");

Results are:

Filter with values(): 399.456ms
Filter with forEach(): 374.698ms

Or you could just stick with the Array method:

const arrMethod = (set) => {
  // Using Array method
  const filter = [...set].filter((item) => item % 2 === 0);
  return filter;
};

let setArray = generateSet(5000000, 10000000);
console.time("Filter with array");
filterForEach(setArray);
console.timeEnd("Filter with array");

It seems consistently faster...

Filter with values(): 356.486ms
Filter with forEach(): 386.825ms
Filter with array: 342.358ms
-2
const set = new Set([1,2,3,4,5]);

function filterSet(index) {
    set.delete([...set][index]);
}

filterSet(3); // Set { 1, 2, 3, 5, [size]: 4 }

thought this was a pretty decent solution for "filtering" the set.

  • 1
    Hi there, sorry but I really need to caution – no, *forbid* – against doing this. Converting to and from an array is common, but Set makes no guarantees that the ordering will be preserved. In your example here *it happened to be the same*, but in general that is not true. A very easy way to break your code looks like this: `const set = new Set([1, 1, 2]); filterSet(2); // what does this even mean now?` – colinta Dec 19 '22 at 17:07
  • RIP, yeah I guess this would error. – Tjerk Pietersma Jan 10 '23 at 23:47