It would be useful to have the ability to get all unique objects according to some provided definition of equality.
Additional functionality:
Array.prototype.uniqBy
There are many implementations of this function across Github, see here. This shows the desire for such functionality. Systems built around custom implementations of this could be subject to bugs or misunderstanding around the differences in equality methods used. This doesn’t account for other implementations of this method which are implemented using another name.
Developers are forced to either implement this functionality themselves, or rely on third party libraries such as Lodash, which offers the same interface as proposed here.
Lodash’s uniqBy function alone has almost half a million weekly downloads. This sits about high up the table for their most commonly used functions. It is their 7th most popular out of the 65 base functions the libraries comes with. See a comparison table for Lodash function popularity here.
Ramda’s uniqBy is another popular function implementation. Their complete library has over 5 million weekly downloads.
There are a couple of ways this functionality could be implemented. Two implementations are listed below.
One approach is to pass uniqBy a function which maps the objects in the array to a canonical representative of an equivalence class.
[ {a: 1, b: 1}, {a: 1, b: 2} ].uniqBy(x => x.a); // [ {a: 1, b: 1} ]
[ {a: 1, b: 1}, {a: 1, b: 2} ].uniqBy(x => x.b); // [ {a: 1, b: 1}, {a: 1, b: 2} ]
[ {a: 1, b: {c: 2}}, {a: 2, b: {c: 2}} ].uniqBy(x => x.b.c); // [ {a: 1, b: {c: 2}} ]
Implementation 1.a:
Array.prototype.uniqBy = function(generateRepresentative) {
let representatives = this.map(generateRepresentative);
let uniqueRepresentatives = Array.from(new Set(representatives));
return uniqueRepresentatives
.map(x => representatives.indexOf(x))
.map(index => this[index]);
}
This implementation works well when mapping to a single value representative. However, becomes problematic when you wish to uniqBy a combination of values.
This could be improved by using the Immutable Records proposal. Immutable Records have built-in value equality which could be used in the following way to support uniqBy on multiple values:
Implementation 1.b:
[ {a: 1, b: 1}, {a: 1, b: 2} ].uniqBy(x => #[ x.a, x.b ]); // [ {a: 1, b: 1}, {a: 1, b: 2} ]
Both versions of implementation 1 have O(n) time complexity.
An alternative implementation of uniqBy would consume a comparison method which would determine whether two entries in the list should be considered equivalent. This way of implementation is referred to as uniqWith
by both Lodash and Ramda.
[ {a: 1, b: 1}, {a: 1, b: 2} ].uniqBy((x, y) => x.a === y.a); // [ {a: 1, b: 1} ]
[ {a: 1, b: 1}, {a: 1, b: 2} ].uniqBy((x, y) => x.b === y.b); // [ {a: 1, b: 1}, {a: 1, b: 2} ]
[ {a: 1, b: {c: 2}}, {a: 2, b: {c: 2}} ].uniqBy((x, y) => x.b.c === y.b.c); // [ {a: 1, b: {c: 2}} ]
Implementation 2.a:
Array.prototype.uniqBy = function(compare) {
let out = [];
this.forEach(x => {
if (!out.some(y => compare(x, y))) {
out.push(x);
}
})
return out;
}
This implementation yields an unideal performance time, ~O(n^2).
The API allows for a comparator that does not actually define a partial ordering, which means a user could use this implementation in unintended ways. There are no safeguards around someone passing (x, y) => x.a > y.a
to the compare function, which could yield confusing results.
Additional information:
There is a node module, array-unique, which has similar behavior. This module has over 13 million weekly downloads. However, this implementation only for doing so on a single value.