SE-0266 — Synthesized Comparable conformance for enum types

Thanks for the proposal, it seems fine to me with or without raw value support, where I can see the arguments either way. The analogy with case ordering already having a big impact on enums with integer raw values is a good one.

I did find this sentence from the introduction confusing:

because of the double negative and the unclear association of the “not themselves conforming to Comparable” clause with the raw and associated values. It wasn't clear to me what you were proposing until I got to the example in the “Detailed design” section.

Edit:
That example itself is also a little confusing because of the interaction between the case ordering and the Int ordering. I suppose it is implying that .premium(0) is “better” than .premium(1)? It shows one of the pitfalls here, I suppose, where you can unthinkingly mix an enum ordered from best to worst with an associated type ordered from smallest to biggest or dimmest to brightest and get an unnatural order.

+1. This is a great and natural addition to Swift.

Like @Nevin and @allevato I think we should support the comparison of raw value enums using declaration order as well. Another argument for using the declaration order regardless of raw values or not is that CaseIterable synthesis already does exactly that:

enum Month: String, CaseIterable {
    case january
    case february
    case march
    case april
    ...
}

Month.allCases // [january, february, march, april, ...]

It would violate the principle of least surprise if synthesized Comparable and CaseIterable ordered the cases differently.

20 Likes

I think it‘s okay to say that the default order is used like this. If you need another order you could opt out and manually implement the alteration.

I think in the future we could introduce custom attributes unlike property wrappers which could reduce the boilerplate and inject additional information into the compiler which then will be used during synthetization.

@comparator(>) // something like this
case premium(Int)

Sure, I think we can live with it, but I would personally change the example in the proposal because, at least in my head, the Membership enums are “upside down”.

2 Likes

I agree with the majority that inclusion of RawRepresentable enums is warranted with the default implementation keeping declaration order as the sort order. If a different sort order is wanted, that can be explicitly implemented to override the default.

One thing I’d like clarified: Is this opt-in by declaring a conformance to Comparable without implementing the protocol’s requirements, or is the idea that all enums that meet the criteria are implicitly comparable? For example, is this

enum Priority {
  case low
  case medium
  case high
}

different from this?

enum Priority : Comparable {
  case low
  case medium
  case high
}
  • What is your evaluation of the proposal?

It feels incomplete. For example, the proposal does not discuss enums with cases that have multiple associated values, all of which are Comparable.

I'm going to guess that the intent in this case is lexicographical ordering. If that is the case, it isn't clear why using lexicographical comparison of enums with multiple associated values should be considered acceptable where synthesis of lexicographical comparison of struct properties using source order is not. If both are acceptable, the proposal should include synthesis of Comparable for structs. If lexicographical comparison is never acceptable, the proposal needs to more clearly document its limitations. If there is a reason it is acceptable for associated values but not for struct properties, that reason should be stated clearly.

Aside from these changes, if we're going to adopt the approach of @memberwise for opt-in synthesis suggested by the Differentiable Programming Mega-Proposal maybe this would be a good place to start.

  • Is the problem being addressed significant enough to warrant a change to Swift?

Yes, the author makes a good case for supporting opt-in synthesis of Comparable for enums based on case order.

  • Does this proposal fit well with the feel and direction of Swift?

I think it has some rough edges that need work, but otherwise it is consistent with the general approach used in Swift.

  • If you have used other languages or libraries with a similar feature, how do you feel that this proposal compares to those?

n/a

  • How much effort did you put into your review? A glance, a quick reading, or an in-depth study?

A quick read.

2 Likes

The proposal does say at the end of its introduction paragraph:

The synthesized comparison order would be based on the declaration order of the enum cases, and then the lexicographic comparison order of the associated values for an enum case tie.

However, perhaps a concrete example would have made this more evident.

Ahh, my apologies for missing that detail. I agree that an example would make this more evident. My questions were all based on this assumption so they still stand. Is lexicographic comparison acceptable for associated values but not struct properties? If so, why? What makes these different? (If they are not different then structs are glaring omission from the proposal)

Enum cases are essentially just a flat list, and presently—aside from indirect—carry no modifiers. This is reflected by the CaseIterable protocol, mentioned above. When you list the cases in an enum in source, even if they're interleaved with other declarations, there's really only a single natural order among them, and IMO there aren't really any other reasons/factors by which you would want to reorder them.

Struct properties, on the other hand, can have a variety of modifiers—visibility, weakness, laziness, to name a few. Some of these are external factors here that might compel the user to use a particular source organization: for example, they might prefer to have all properties grouped by visibility (all public API at the top of the type, all private API at the bottom, or something). Forcing a source order in that case in order to synthesize the correct implementation feels like a much tougher constraint.

5 Likes

I'm not challenging the way ordering of cases is interpreted. I think that is reasonable (although I can conceive of counter-arguments).

What I am questioning whether lexicographic ordering of multiple associated values in the same case is a good idea or not. And if it is, why are struct properties so different?

+1 This will be very helpful.

Because, as I mentioned above, struct properties have other characteristics that might compel their source organization, but the list of associated values does not—they are another flat list, and they have no modifiers to compel the user to order them differently than the natural comparison order if that is their intent (which they are indicating by placing them in that order and explicitly declaring the conformance).

Also unlike struct properties, associated values can be thought of effectively as a tuple associated with the case (whether implemented that way or not under the hood), and the standard library already has implementations of < for tuples up to a specific arity (and likely would for arbitrary arity, if it were possible to conform them to Comparable).

3 Likes

In addition to what @allevato just wrote, I’ll also point out that the existing opt-in synthesis of Equatable for enums works fine when there are cases with multiple associated values, and it would be unexpected if Comparable didn’t.

1 Like

That wouldn't be a good argument as struct also has Equatable synthesis.

What sets Equatable and Comparable apart is that the order of declaration matters.

3 Likes

The proposed comparable seem to only take into account cases which for an enum can not be added via extension. This suggest to me that maybe the name for should CaseComperable to reflect that this only applies to enums cases similarly with CaseIterable

1 Like

Why? Under that basis, shouldn't Equatable and Hashable have to be called CaseEquatable and CaseHashable for enums, then?

The cases and associated values of an enum precisely define the values of an enum, and those values are what are being compared. It's not clear what something being added via an extension applies to here.

1 Like

I often organize my associated values for readability and clarity. I haven't found myself wanting Comparable conformance for these enums, but if I did this may not correspond with the natural comparison order. I don't find modifiers to be any stronger a reason for determining source order than these concerns.

If we're going to trust that users opting-in to Comparable synthesis want source order for associated values I think we should consider extending the same trust to structs and properties. We should certainly have a stronger reason for treating structs differently than enums than "sometimes people like to group declarations with the same modifiers together".

Tuples are anonymous product types and structs are nominal product types. I don't see a strong distinction here. There is a wide range of complexity in structs. Many are as simple as tuples are, but with a name for the type.

The existing opt-in synthesis of Equatable for structs works fine as well. I'm sure some people would find it unexpected for Comparable synthesis to work on enums but not structs.

I don't have a strong opinion about the right direction, but I think the proposal as drafted sits in a valley of inconsistency.

1 Like

from 185

The original discussion thread also included Comparable as a candidate for automatic generation. Unlike equatability and hashability, however, comparability requires an ordering among the members being compared. Automatically using the definition order here might be too surprising for users, but worse, it also means that reordering properties in the source code changes the code's behavior at runtime.

I was thinking about declaration order and if people can extend something then depending on the order those extensions load it would change the final order. Specifically I was thinking about supporting raw types like strings, it would be less confusing if it said CaseComparable. But if we are not going to support raw types then probably okay to stick with just Comparable

That's too reductive; the source order of fields in tuples matters, even if the fields have names.

  1> (foo: 1, bar: 5) == (bar: 5, foo: 1)
$R0: Bool = false

The source order of structs matters much less; it only affects ABI, which is not relevant here, and the signature of the synthesized memberwise initializer, which I'm willing to concede because it's never public API and its behavior is never affected.

Likewise, you can't reorder the associated values of an enum case declaration and retain source compatibility with the usage sites. That changes its public API.

So, I think it's still consistent to tie Comparable synthesis to things that can be reordered in source without affecting public API/source compatibility.

3 Likes

Even if we support raw value enums, I still don't see how extensions come into play here, or in the SE-0185 case. SE-0185 implements Equatable and Hashable in terms of stored properties for structs and cases for enums, neither of which can be added to a type via an extension. Everything used to determine the synthesized ordering of a value is found in the primary type declaration, and this proposal wouldn't change that.

1 Like
Terms of Service

Privacy Policy

Cookie Policy