Partially Equatable/Comparable?

Reading Apple's site's notes on Comparable states that a type may have states that don't compare to anything. A comparison operator should return false if at least one operand is in the incomparable class. This violates the expected not-conditions the various operators have with each other (== opposes !=, < opposes >=, > flips < and opposes <=); this philosophy lies to the user.

If only there was some way for a type to Optional-ly declare comparisons. Hmm....

protocol PartiallyEquatable {
    static func ==?(_ lhs: Self, _ rhs: Self) -> Bool?
}

extension PartiallyEquatable {
    static func !==?(_ lhs: Self, _ rhs: Self) -> Bool? {
        guard let result = lhs ==? rhs else { return nil }
        return !result
    }
}

protocol PartiallyComparable: PartiallyEquatable {
    static func <?(_ lhs: Self, _ rhs: Self) -> Bool?
}

extension PartiallyComparable {
    static func >?(_ lhs: Self, _ rhs: Self) -> Bool? {
        return rhs <? lhs
    }

    static func <=?(_ lhs: Self, _ rhs: Self) -> Bool? {
        guard let result = lhs >? rhs else { return nil }
        return !result
    }

    static func >=?(_ lhs: Self, _ rhs: Self) -> Bool? {
        guard let result = lhs <? rhs else { return nil }
        return !result
    }
}

The next problem is that the Partial and regular comparison protocols are related, but which way should the implementations go. There are arguments for each direction.

For integer types, the partial comparison operators should trivially forward to the regular versions:

protocol ActuallyEquatable: Equatable, PartiallyEquatable {}

extension ActuallyEquatable {
    static func ==?(_ lhs: Self, _ rhs: Self) -> Bool? { return lhs == rhs }
}

protocol ActuallyComparable: Comparable, PartiallyComparable, ActuallyEquatable {}

extension ActuallyComparable {
    static func <?(_ lhs: Self, _ rhs: Self) -> Bool? { return lhs < rhs }
}

protocol BinaryInteger: ActuallyComparable { /*...*/ }

(I don't know if Swift allows this kind of partial specialization.)

For floating-point types, we flip the dependencies to maintain the lie IEEE-754 demands:

protocol FakingEquatability: PartiallyEquatable, Equatable {}

extension FakingEquatability {
    static func ==(_ lhs: Self, _ rhs: Self) -> Bool { return (lhs ==? rhs) ?? false }
    static func !=(_ lhs: Self, _ rhs: Self) -> Bool { return (lhs !=? rhs) ?? false }
}

protocol FakingComparability: PartiallyComparable, Comparable, FakingEquatability {}

extension FakingComparability {
    static func <(_ lhs: Self, _ rhs: Self) -> Bool { return (lhs <? rhs) ?? false }
    static func >(_ lhs: Self, _ rhs: Self) -> Bool { return (lhs >? rhs) ?? false }
    static func <=(_ lhs: Self, _ rhs: Self) -> Bool { return (lhs <=? rhs) ?? false }
    static func >=(_ lhs: Self, _ rhs: Self) -> Bool { return (lhs >=? rhs) ?? false }
}

protocol FloatingPoint: FakingComparability { /*...*/ }
1 Like

There have been at least two very extensive discussions on Equatable and Comparable in relation to floating-point values, including two or three draft proposals.

The core team outlined their reasons for not having "partial" versions of these protocols based on the experience of Rust and other languages.

I would urge you to review and summarize those discussions first before launching into this topic again, as there was a lot of ground already covered. This is a very difficult area design-wise.

I cannot fully agree. It is a big question whether this even is a problem, and the efforts of trying to find ideas to resolve this AFAIK only introduce code mess and impracticality.
The thing is, all numeric types have a NaN risk and even so, they are all comparable. Simply because mathematics.

An indeterminate form is referenced to in programming as 'not a number' (nan). In fact, formally it is an expression that can be equal to any number in some set (obviously the cardinality should be > 1). Therefore it is senseless to compare it to numbers and there's nothing wrong with that (almost).

Any comparable type has the ability to compare objects of the same type, but there is no guarantee that a type is an algebraic group, closed under a collection of operations it uses.

Comparing numeric types and not only is a crucial routine in programming and must remain simple.
Imagine doing this each time:

if let success = (someNumber == anotherNumber) {

   if success { ... }
}

Getting and using NaN in code by far isn't your everyday practice and in the end you can just go with number.isNaN

What I believe is a slight problem though is the actual ability to compare a number to NaN.
NaN is not a number (excuse the tautology), and could be considered a separate type.
Then, there would be no chance for it to slip into a comparison function, which is what we all want.
However, this means we can return different types as a result of a single operation, which implies impermissible type-unsafety.