Synthesizing Comparable, for enums

It seems to me like we can do both. Whatever design we come up with for synthesizing Comparable would also be useful as an example for what generalized deriving ought to look like.

first draft here: swift-evolution/0000-synthesized-comparable-for-enumerations.md at patch-5 · kelvin13/swift-evolution · GitHub

Regarding having declaration order be meaningful, SE-0260 already defines source order as meaningful under -enable-library-evolution mode, as described here.

This reminds me of how the precedence of operators used to be ordered. The approach wherein the order of the cases for comparison is synthesized rather than stated with large gaps between numbers is ideal as there is increasing flexibility. It seems like a generally unnecessary hindrance for quick and easy Comparable implementation on a basic enum. Also the fact that it requires maintenance upon the addition of a new case, seems just generally undesirable and avoidable.

extension ValueComparable where Self: RawRepresentable, RawValue == Int {
    var comparableValue: Int { return rawValue }
}
extension ValueComparable where Self: CaseIterable, AllCases.Index == Int, Self: Equatable {
    var comparableValue: Int { return Self.allCases.firstIndex(of: self)! }
}

It seems to me that compiler-generated implementations fall longer-term into the category of 'acts like macro in lieu of macros', and that these special implementations look like

enum Foo : @CaseIterable, @CaseComparable  { ... }
2 Likes

Hi all, i’ve got the implementation running at https://github.com/apple/swift/pull/25696, and you can try it out now.

proposal PR: Create 0000-synthesized-comparable-for-enumerations.md by kelvin13 · Pull Request #1053 · apple/swift-evolution · GitHub

1 Like

That's certainly one approach. They might also be expressible as default implementations in protocol extensions, e.g.:

// default implementation for enums whose cases are all Comparable
extension Comparable where EnumCases...: Comparable { ... }

well, right now the feature as implemented doesn’t apply to enums with associated values so the EnumCases...:Comparable constraint isn’t needed.

Related bug report from a couple of years ago Expose the declaration order of Enum cases with payloads

it’s been suggested that lexicographic comparison on enums with associated values might get added onto this proposal. what does everybody think about this idea?

Presuming you mean declaration order and not lexicographical order, can you clarify what the behavior would be?

like declaration order first and then if they have the same case the payloads break the tie

Even though it seems slightly magical, since you point out that source-level of reördering enum cases in raw-value enums already has behavior-changing effects, I suppose there's at least precedent for similar behavior in the case of Comparable. So I think it's reasonable.

There are still a couple points that should be explicitly called out:

  • As with other synthesized conformances, it's "all or nothing": either all of the cases' associated values conform to Comparable and the enum gets Comparable conformance synthesized, or Comparable conformance cannot be synthesized. There should not be a situation where an enum with associated values gets a synthesized conformance based on ordinal alone.

    Note that this does not solve the problem in @masters3d's reported issue above; that's intentional, because I consider "partial information" scenarios like that to be a special case that should require manual intervention.

  • If an enum with raw values is declared to conform to Comparable, then even if the raw value's type conforms to Comparable, the synthesized implementation is based on declaration order, not the underlying implementation of the raw type.

    This would be for consistency across all enums (principle of least surprise). It's trivial for a user to manually delegate to the other implementation if it's what they want, since that's already what they have to today: static func < (lhs: T, rhs: T) -> Bool { lhs.rawValue < rhs.rawValue }

2 Likes

Note, lexicographic comparison for the tie break would be consistent with how the implementation of < for tuples works.

This would not generally be compatible with the requirement of Equatable (and hence Comparable) which require substitutability. It is very rare for associated values to lack meaning, and it definitely shouldn't be the default synthesized behavior when the elements aren't comparable. I see this as similar to Equatable synthesis for structs: if you have one non-equatable element, sorry, you have to write == yourself. Yes, that's a pain, but fixing that has different solutions unrelated to this feature, which covers the I would guess easily-99% case.

This sounds right to me.

2 Likes

Right—that's the same contradiction that I called out in a reply further up the thread when it was suggested previously. But since it's been mentioned as motivation in at least a couple places (the issue linked in this comment and the comment I replied to), it feels important to write up in the final proposal that it's explicitly a non-goal and explain why that's the case (to short-circuit future questions about why it wasn't done).

1 Like

i’m not a fan of enabling this feature for enums with raw values, it would be too confusing to the user.

2 Likes

That’s a reasonable starting position certainly, and could always be loosened later.

3 Likes