Currently Comparable inherits from Equatable, but does not provide a default implementation for ==, so the compiler tries to synthesize one composed of member-wise ==s. This leads to a problem where if a type's < is not composed of member-wise inequalities, then <, >, and == all evaluate to false for some pairs of values. For example:
struct Length {
enum Unit: Double, Comparable {
case mm = 0.001
case m = 1
case km = 1000
case banana = 0.178 // according to bananaforscale.info
}
let magnitude: Double
let unit: Unit
static func < (lhs: Self, rhs: Self) -> Bool {
lhs.magnitude * lhs.unit.rawValue < rhs.magnitude * rhs.unit.rawValue
}
}
let aBanana = Length(magnitude: 1, unit: .banana)
let anotherBanana = Length(magnitude: 0.178, unit: .m)
print(aBanana < anotherBanana) // prints 'false'
print(aBanana > anotherBanana) // prints 'false'
print(aBanana == anotherBanana) // prints 'false'
Logically, a = b iff a ≮ b and a ≯ b, so one would reasonably think that aBanana == anotherBanana, because neither the < nor the > evaluates to true. This is also the source of a SwiftPM bug where one package version can be neither equal to nor greater/lesser than another at the same time.
Additionally, the current behaviour does not match Comparable's documentation:
Types with Comparable conformance implement the less-than operator (<) and the equal-to operator (==). These two operations impose a strict total order on the values of a type, in which exactly one of the following must be true for any two values a and b:
I tried to think of an example where the status quo is preferred, but can't come up with a good one.
Adding a default implementation of == to Comparable like this:
extension Comparable {
@inlinable
static func == (lhs: Self, rhs: Self) {
!(lhs < rhs || lhs > rhs)
}
}
is silently source breaking, but likely the sources it breaks are already incorrect.
What does everyone think?