A sort using a comparator1 is more universal and flexible than one which merely uses a key-selector. However, this does not mean that one approach is necessarily "better" and it comes down to implementation and use.
First off, a comparator has one purpose - to establish ordering between to arbitrary values. This operation is used by the sort function, and such a means of ordering values must exist at some level.
Consider that a key-selector or merely a handy convenience to define such a comparator, where <=>
defines some natural ordering (note that <=>
is itself a comparator!). Thus, the key-selector is actually an input transformation to a built-in comparator.
def comparator (a, b):
return a.x <=> b.x -- explicit value selection
def comparatorForKeySelector (keyFn, a, b):
return keyFn(a) <=> keyFn(b) -- keys selected by function
As stated at the start, the selection comes down to implementation and use.
The first candidate, underscore.js's sortBy happens to be a limited implementation; because the underlying comparator does not understand how to order returned arrays/sequences it is not possible to sort on multiple values without hacks such as string munging. It also fails to provide the ability to reverse the sort. These are implementation issues which could both be addressed with an appropriately modified built-in comparator.
The second key-selector candidate, Python's keyed-sort, can trivially sort on multiple keys, thanks to ordering between tuples. It also allows the sort direction to be specified making it a much better implementation. However, it does not allow sort direction to change between the keys (e.g. sort the first key ascending the second descending), which is also due to the ordering between tuples.
Another key-selector candidate, C#'s LINQ which has neither of the limits above because of chaining with explicit sort directions: eg. people.OrderBy(p => p.Height).ThenByDescending(p => p.Weight)
. I consider this the "ideal" key-selector implementation, albeit one that relies on some pretty nifty underlying support.
After looking at the three different implementations, noting that they can solve a common class of problems with varying degrees of success and flexibility, I refer back to my introduction: a comparator is more universal than a key-selector and I think the "better" solution is to support both [well], such that a particular problem can be solved in the cleanest way possible.
1 I am using the term comparator to consistent refer to a function - possibly an operator - that establishes ordering between two values. At the primitive level this might be implemented merely with a relational and equality operator pair.
A la carte:
Behavior is undefined when the function is not transitive, or if it's not antisymmetric.
Then the function is broken and should be fixed. Broken code is broken code and cannot be used as an argument against using a comparator.
The expected return value, an int in the set {-1, 0, 1}, is awkward
This is a pretty natural way to express this; many sorts accept "less than zero", "zero" and "more than zero" values. These could be typed more explicitly - eg. with constants - but have no bearing on the semantics of such a function.
It's longer than the keyfunc approach and uses imperative style when declarative style would do nicely
A comparator is not implicitly an "imperative style". In a functional language it will be a "function style". The key-selector is often more terse and clear; but this can be argued for most specialized API.