[1,2,10].sort()
= [1,10,2] [1,2,10].sort()
= [1,10,2]When neither the function nor the parameters have hard types, you have to create heuristics. There could be a test for numbers there, but it would also be surprising because at the older days people expected "10" and 10 to behave the same.
[1, 10, 2].sort((a,b) => a > b ? 1 : -1)
// -> [1, 2, 10]
is something like // [1, 10, 2].sort((a,b) => a.toString() > b.toString() ? 1 : -1)
// -> [1, 10, 2]
because someone thought that it was "best" to cast stuff to string in case whatever you put in the container didn't implement comparison.A JavaScript sort comparator is required to not only return a positive or negative value, it must handle all three cases: greater than, less than, or equal. When the arguments compare equal, the comparator must return 0.
Returning only a positive or negative value is a very common error. (I don't mean to pick on you for this! I have seen the error so often that I hope this may be helpful for anyone writing a sort comparator.)
The invalid comparator will force the normally stable sort to be unstable, because sort() now has no way to know that two items should be considered equal and their existing order preserved.
Let's demonstrate with the "doggos" example that the article links to:
https://mathiasbynens.be/demo/sort-stability
The code starts with this array:
const doggos = [
{ name: 'Choco', rating: 12 },
{ name: 'Devlin', rating: 13 },
{ name: 'Eagle', rating: 13 },
{ name: 'Jenny', rating: 13 },
{ name: 'Kona', rating: 13 },
{ name: 'Leila', rating: 13 },
{ name: 'Milly', rating: 14 },
{ name: 'Molly', rating: 12 },
{ name: 'Nova', rating: 12 },
{ name: 'Oliver', rating: 13 },
{ name: 'Patches', rating: 14 },
];
It uses this valid comparator: doggos.sort( ( a, b ) => b.rating - a.rating );
(Note that this implements a reverse sort as can be seen in the output on the test page.)Subtraction provides the proper return values when two numbers are compared: any negative number, any positive number, or zero when the ratings are equal.
Another valid comparator (again using a reverse sort) would be:
doggos.sort( ( a, b ) =>
a.rating < b.rating ? 1 :
a.rating > b.rating ? -1 :
0
);
Either way you get the expected output in the current version of Chrome, which has a stable sort. Each group of doggos with the same rating appear in their original order: [
{ name: "Milly", rating: 14 },
{ name: "Patches", rating: 14 },
{ name: "Devlin", rating: 13 },
{ name: "Eagle", rating: 13 },
{ name: "Jenny", rating: 13 },
{ name: "Kona", rating: 13 },
{ name: "Leila", rating: 13 },
{ name: "Oliver", rating: 13 },
{ name: "Choco", rating: 12 },
{ name: "Molly", rating: 12 },
{ name: "Nova", rating: 12 }
]
So let's go back to the original doggos array and use a sort function that only returns 1 or -1 (once again using a reverse sort): doggos.sort( ( a, b ) =>
a.rating < b.rating ? 1 : -1
);
With this function, the result is: [
{ "name": "Patches", "rating": 14 },
{ "name": "Milly", "rating": 14 },
{ "name": "Oliver", "rating": 13 },
{ "name": "Leila", "rating": 13 },
{ "name": "Kona", "rating": 13 },
{ "name": "Jenny", "rating": 13 },
{ "name": "Eagle", "rating": 13 },
{ "name": "Devlin", "rating": 13 },
{ "name": "Nova", "rating": 12 },
{ "name": "Molly", "rating": 12 },
{ "name": "Choco", "rating": 12 }
]
Now the sort has become unstable. Doggos with the same rating are no longer in their original order.Bottom line, be sure to handle all three cases in your sort comparator if you want a stable sort.
A good alternative for statically typed languages is to have the comparison result be a proper sum type, and provide ways to generate and compose comparisons e.g. Haskell or Rust's Ordering (and Ord trait / typeclass).
If the comparison function argument was mandatory in sort() this wouldn’t be a problem.
Stupid defaults is not "the curse of optional arguments", it's the curse of stupid defaults.
Most languages have defaults for these two operations yet have proper defaults rather than stupid ones. Python's list.sort() and int() certainly do, so do Java's Collections.sort() and Integer.parseInt().
Uniquely awful defaults is a historical (and defining) feature of javascript, not of having default values, or optional arguments.
[1,2,10].map(parseInt)
It’s several things that on their own aren’t mad which taken together produces a crazy result. 10 as the default for an optional second argument of parseInt is fine. But map should only use a single argument closure by default. So map(parseInt) must be equivalent to map(x -> parseInt(x)).
(a, b) -> a.toString().compareTo(b.toString())
Unlike say int.parse() where 10 is a reasonable default for radix, doing string comparison as default is almost never what the caller wants, so is a bad default. Functions where there is no obvious (99% of the time the desired value) should not have a default value when the argument is omitted. I’d much rather have things.sort() just fail and force me to specify the comparison, than to see it sort alphabetically.
As a side note: a second optional argument specifying stability true/false would be reasonable. True could be the default too, since unstable sorting is effectively an optimization with a tradeoff.
Hell, really?? That's gross.
As for the parent's comment about a dev "not realising array.sort() was unstable", the dev should know his tools. I can't see anyway why a stable sort is better, I've never sorted on x then sorted on x, you just sort on x & y.
It's just using the == operator, no? Which, yes, is really gross.