Many examples of more complicated "comparables" or "comparators", often in statically typed languages, have been given, so let's take a look at languages that aim to be simple.
First, comparators should not be tied to objects. I want to be able to sort the same people both by age and name. Thus, we need some kind of "comparator" interface. You asked
How to define the minimum interface and ensure that there is no conflict?
The simplest way is to just require a function(a, b T) bool, a "less than" function, as a comparator. This function should return true if and only if a < b. This is what Lua's and Go's sort functions do.
Now developers just need to implement one function correctly. The rest follows from the definition of "less than":
- If
a < b, then a is less than b. - If
b < a, then a is greater than b. - Otherwise,
a and b must be equal (assuming a total order)
Want to sort by age? Simply pass function(a, b) return a.age < b.age end.
The advantage of this approach is that it's about as simple as can be - no "spaceship" operator, no need for an "enum" - or worse, signed number, which invites using arithmetic, which runs the risk of overflows - return type, no need for a "comparator" interface/trait, just a simple boolean function.
Worth mentioning here: Python functions that require an ordering let you pass a key function (rather than a comparator). This is often slightly more convenient and reduces the potential for mistakes - often you just want to sort by a property (perhaps one that you haven't calculated yet), or a bunch of properties (in which case you can pack them up in a tuple).
There are downsides to this approach, though. For one it can be slightly less efficient due to having to make (up to) two calls (but only 1.5 on average, assuming < and > appear equally often and == is rare) calls to the "comparator" rather than a single call. It also assumes a total order, usually. I find that this is not a major limitation, though. The most common applications where orders are needed require a total order. Floats may technically have NaN, but nobody would expect sorting a list with NaNs in it to produce a sensible result; the NaNs ending up in the list would be considered a bug, and it is welcomed if sorting throws an error.
Now, Lua also has "metamethods" to redefine the behavior of the comparison operators for tables. Some things Lua did right here:
- There are no separate metamethods for
~=, > or >=. a ~= b is just syntactic sugar for not (a == b). It does not let programmers redefine ~= to potentially be inconsistent with ==. a > b and a >= b are just syntactic sugar for b < a and b <= a respectively as well. == is special: If objects are "primitively equal" (equal by reference), they are always equal and the metamethod is not called. This also makes sense (and saves the programmer from writing the if rawequal(a, b) then return true end optimization boilerplate themselves). One thing that is debatable is that Lua only calls the metamethod if both operands are tables (/ full userdata) - that is, if you implement a custom number type (say bigints), you can't implement comparisons against primitive numbers (this is relevant to performance, however: Checks can be more efficient if there is no need to check the metatable; this does not apply to <, because a "raw" table < primitive is of course an error, whereas a "raw" table == primitive is just false). It is also inconsistent with hashing, as there is no way to provide a custom "hash" metamethod. - This leaves us with
< and <= metamethods. Older Lua versions would "emulate" a <= b using not (a < b) if it was missing, automatically ensuring consistency, newer Lua versions (5.4+) don't, probably because it makes it easier to understand which metamethods are called and why. == could be emulated with < or <= in principle. But this is not a good idea for performance. == can often be faster than < or <= - for example all (only "short" ones in newer versions) strings in Lua are interned, so string comparison for equality is O(1), whereas lexicographic comparison can very well be O(n).
Another interesting case is Haskell. If you look at Haskell's Ord typeclass, it gives you a "best of both worlds": Either <= or compare are enough for a "minimal complete definition" such that the compiler fills out the rest.
To recap: A single < or <= function in principle suffices to "emulate" all other comparison methods (for a total order). There are tradeoffs, which is why you might want to allow for multiple, technically "redundant" implementations for some of these methods. A "spaceship"-style operator can efficiently unify the comparison operations, but is not as simple as can be.
Some languages have facilities to autogenerate these (like #derive in Rust) or implementations which let you order by (computed) "key" (like Python).