Is there a reason NSDecimalNumber is not Comparable?

NSDecimalNumber is Equatable but not Comparable.

Now there is a strong suggestion not to conform types from the standard library to protocols from the standard library yourself, but otherwise it would be trivial to add:

extension NSDecimalNumber: Comparable {
    public static func < (lhs: NSDecimalNumber, rhs: NSDecimalNumber) -> Bool {
        return lhs.compare(rhs) == .orderedAscending
    }
}

I wonder if this is an intentional omission?

2 Likes

Cc @Philippe_Hausler @scanon

NSDecimalNumber’s bridged variant, Decimal, does conform to Comparable. Is there a reason you need to keep these numbers in their reference form?

NSDecimalNumber is not a standalone class -- it is part of a class hierarchy. It subclasses NSNumber, and that superclass would arguably be a better point to introduce a Comparable conformance.

Operators such as < and == aren’t overridable in Swift, so class hierarchies that conform to Comparable typically need to provide overridable hooks to let subclasses customize the implementation. Luckily, NSNumber already provides the overridable method NSNumber.compare(_:) to implement a heterogeneous three-way comparison, and it customizes NSObject.isEqual(_:) to call it. So it would be technically possible to add the Comparable conformance without changing Foundation's Objective-C API.

However, it wouldn’t necessarily be a good idea. For bridged types, we generally prefer to keep the battle-proven Cocoa APIs as pristine as possible, and we spend most effort on improving the API integration of the corresponding Swift type instead. For example, while Array is a fancy-pants mutable random-access range-replaceable ordered collection, but NSMutableArray is merely a sequence.

The assumption is that people would prefer to use Swift-native bridged types in most cases. This is encouraged by how Swift automatically bridges parameter types while importing Objective-C APIs. However, when Swift code needs to work with the original Cocoa types, it is generally more useful to work with the original Cocoa API, with little to no Swift-only additions. (Except for the usual, mostly mechanical naming & type signature transformations.) For example, NSMutableArray’s API would be rather confusing if it supported RangeReplaceableCollection requirements in addition to its own methods.

This is not a theoretical concern; we’ve seen how subtle details of NSObject’s Equatable/Hashable conformance has caused a great deal of confusion over how to customize equality/hashing in its subclasses. People don’t subclass NSNumber very often, but why add another pitfall? compare(_:) is already tricky enough to implement on its own — and adding a ‘<‘ overload would likely lead to the same problems as NSObject’s definition for ==.

2 Likes

Yes, for KVO and Cocoa interaction. As long as Cocoa doesn't provide a Swift native KVO implementation using those pesky Objective-C types is the only option (unless willing to introduce some massive dependencies)

Your arguments are of course totally sound and understandable, but why is NSDecimalNumber Equatable then? Also: using lhs.compare(rhs) == .orderedAscending is super un-Swifty :cry:

1 Like

That’s a good reason! You could still bridge to Decimal whenever you want to manipulate these values, though — bridging from NSDecimalNumber is reasonably cheap (both in terms of runtime performance and source complexity), and you get arithmetic operators in addition to </==.

I’d write that as (lhs as Decimal) < (rhs as Decimal).

I wasn’t involved in the decision to add Equatable/Hashable conformances to NSObject, but to me those two feel like “obvious” choices — every Cocoa object is comparable for equality using isEqual(_:) and hashable using the hash property, so it seems like a good idea to add these. These conformances allow you to put Cocoa objects in Swift dictionaries, which simplifies some aspects of bridging, and it is a useful feature anyway.

The feeling of obviousness quickly dissipates when we look into the details of even these “easy” conformances. E.g., consider the consequences of the sometimes divergent concepts of equality between bridged Cocoa types and their Swift counterparts. (E.g., NSString vs String.) Throw AnyHashable into the mix, and you can keep an engineer busy for months by asking them to fix just the most obvious issues!

NSNumber is also not the only class that defines a compare(_:) method. While it’s not part of NSObject, it is a relatively common method in Cocoa APIs. Should NSString, NSDate, and NSDateInterval also conform to Comparable? How about AppKit’s NSCell?

4 Likes