A key parameter for Array.prototype.sort

It is useful to provide a key parameter or a mapper function as the sort key while sorting.
Popular libraries like Lodash, Underscore and Ramda already provide this method (sortBy).

array.sort((a, b) => (a.key > b.key) - (a.key < b.key)) may be enough in certain cases, but when the mapper does expensive calculation, one must do something like array.map(value => ({ key: someExpensiveCalculation(value), value })).sort((a, b) => (a.key > b.key) - (a.key < b.key)).map(({ value }) => value). This is verbose and inefficient. This can be solved by appending an optional argument to Array.prototype.sort, which the implementer can optimize further.

It roughly does the following:

function IsCallable(arg) {
	try {
		[].forEach(arg);
		return true;
	} catch {
		return false;
	}
};
const oldSort = Array.prototype.sort;
Array.prototype.sort = function(comparefn, keyOrMapfn) {
	if (typeof keyOrMapfn === "undefined")
		return oldSort.call(this, comparefn);
	if (!IsCallable(comparefn) && typeof comparefn !== "undefined")
		throw new TypeError("Array.prototype.sort requires the comparator argument to be a function or undefined");

	if (IsCallable(keyOrMapfn))
		for (let i = 0; i < this.length; i++)
			this[i] = { key: keyOrMapfn(this[i], this), value: this[i] };
	else
		for (let i = 0; i < this.length; i++)
			this[i] = { key: this[i][keyOrMapfn], value: this[i] };

	if (typeof comparefn === "undefined")
		oldSort.call(this, (x, y) => (String(x.key) > String(y.key)) - (String(x.key) < String(y.key)));
	else
		// Assert: IsCallable(keyOrMapfn) is true.
		oldSort.call(this, (x, y) => comparefn(x.key, y.key));

	for (let i = 0; i < this.length; i++)
		this[i] = this[i].value;
	return this;
};

Yeah, I would like to have this functionality.

Unfortunately I don't know if it will be web-compatible to add an additional parameter; that breaks things sometimes. So it might end up being a new sortBy method instead.

And I think probably it would only take a function, not also a key; sortBy(x => x.y) isn't that much harder to write than sortBy('y'), and is much clearer. (When we added groupBy - now renamed group - we only allowed a function, for the same reason.)

5 Likes

I like the idea, but I would prefer if the {key, value} objects were not leaked. In your sample implementation, they can be accessed when calling sortBy on a proxy or when accessing the array elements during the keyOrMapFn/compareFn calls. And they leave a mess in the array if any of those function calls throws an exception.

I don't think it's that inefficient. You cannot really avoid allocating extra space at least for the "key" values, and probably not avoid allocating some extra housekeeping space around each key (whether an index or just a copy of the value) either.

Which begs the question: should sortBy even reorder the array in-place, or should it be a sortedBy method that returns a new array?

2 Likes

I don't really think the different API shape changes whether it's more useful for in-place or not. Given the change array by copy proposal I think it'd make most sense to just provide both.

Certainly from my own code, there's a mix of .sort(...) for in-place and not in-place (with .slice().sort(...)), I would personally like to eliminate all of them by replacing them with .toSorted(...) and .toSortedBy(...) as appropriate.

Oh it would also be useful to accept multiple to-comparable functions so that one can break ties:

// Sort by age, but for people with same age sort by name
arr.sortBy(person => person.age, person => person.name);

Technically this is possible without multiple to-comparable functions thanks to array sorting being stable now, however it's always somewhat confusing as you need to apply them backwards (which I forget most times):

arr.sortBy(person => person.name).sortBy(person => person.age);

For multiple keys, @theScottyJam once suggested PriorityQueue:

A priority queue is to solve a different problem, but related problem.

What I really want for sorting multiple keys, is for tuples, from the tuple/record proposal to be comparable via < and >, that way you can use them in these kinds of sort functions. This would allow you to do this:

arr.sortBy(person => #[person.name, person.age]);

A tuple will first sort via the first tuple entries, and if it's a tie they'd sort via the second, then third if it exists, and so on.

Unfortunately, they're deciding against doing this for the first iteration of the proposal, leaving the discussion for a follow-on proposal, and even then it's uncertain if it'll ever happen due to the fact that there's conceptually a disconnect with how the equality behaves from <=/>=, and how === behaves on a tuple, which could make for some annoying pitfalls. The full thread can be found here.

1 Like

I suppose

arr.sortBy(person => [person.age, person.name])

have a better perf than

arr.sortBy(person => person.name).sortBy(person => person.age)

Because if ages are differents, it's useless to compare names