SE-0266 — Synthesized Comparable conformance for enum types

The review of SE-0266 — Synthesized Comparable conformance for enum types begins now and runs through October 10, 2019.

Reviews are an important part of the Swift evolution process. All review feedback should be either on this forum thread or, if you would like to keep your feedback private, directly to the review manager (via email or direct message in the Swift forums).

What goes into a review of a proposal?

The goal of the review process is to improve the proposal under review through constructive criticism and, eventually, determine the direction of Swift.

When reviewing a proposal, here are some questions to consider:

  • What is your evaluation of the proposal?

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

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

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

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

Thanks,
Ben Cohen
Review Manager

14 Likes

This looks right at home in Swift, though I would prefer we allow it for raw-value enums as well. The comparison should always use declaration order, since if the cases are organized in a different order than the raw values, there is presumably a meaning and a reason for it. If the enum’s author wants it to be compared by raw value instead, they can trivially provide a one-line implementation of the < operator to do so.

17 Likes

+0.95 in its current form, +1 with support for raw value enums (compared using declaration order).

SE-0266 is proposing that, for enums without raw/associated values or with all Comparable associated values, declaring conformance to Comparable without an explicit implementation of < is a declaration of intent by the user that they want a declaration-order-based ordering. Doing the same for enums with raw values is wholly consistent with that.

The compiler implementation also leads to a potentially odd inconsistency. From what I can tell, this enum would not receive synthesized Comparable:

enum Foo: Int, Comparable {  // ERROR
  case bar = 1
  case baz = 2
}

but writing it this way, which is semantically equivalent, would receive synthesis:

enum Foo: RawRepresentable, Comparable {  // OK
  case bar
  case baz

  init?(rawValue: Int) {
    switch rawValue {
    case 1: self = .bar
    case 2: self = .baz
    default: return nil
    }
  }

  var rawValue: Int {
    switch self {
    case .bar: return 1
    case .baz: return 2
    }
  }
}

So under the current proposal, I would be able to have RawRepresentable synthesis or Comparable synthesis, but not both at the same time. More importantly, if the argument against supporting enums with raw values is about potential confusion about what should be compared, then shouldn't that argument also apply to the second form above?

As @Nevin points out, implementing Comparable.< based on rawValue is easy if that's the behavior you want. Implementing it based on declaration order is extremely difficult or laborious, or this proposal wouldn't exist. So let's just go all the way here; synthesizing it for raw value enums and asking the user to write a one-liner to use a different implementation is IMO a better choice than requiring authors of raw value enums who want declaration-order comparison to implement it as though this proposal didn't exist.

24 Likes
  • What is your evaluation of the proposal?
    +1 for what's proposed, and bonus 0.2 for supporting value backed enums :slight_smile:
  • Is the problem being addressed significant enough to warrant a change to Swift?
    Definitely.
  • Does this proposal fit well with the feel and direction of Swift?
    Yes. It's a clear omission from the language's capabilities that I'm looking forward to being available.
  • If you have used other languages or libraries with a similar feature, how do you feel that this proposal compares to those?
    Swift's enums are pretty unique compared to other languages I've used. The general idea of an enum is often naturally expressed with a clear order. So that order being easier to work will be a welcome!
  • How much effort did you put into your review? A glance, a quick reading, or an in-depth study?
    Participated in some of the pitch discussion, read through the proposal.

I would also appreciate some expanded rational beyond

No enum types with raw values would qualify.

My guess is it's punting on whether they should compare the declaration order or the rawValue, but that's just my speculation and I could see there being a technical reason it's more complicated.

As far as what should be compared, I think I originally preferred the rawValue. In a vacuum that seems to be the "true value" of a case, however given the only natural order for a basic enum is declaration, I don't think the inconsistency is worth being slightly more (IMO) "correct". Especially when custom comparison of rawValue is much easier do manually than using the declaration order.

Anyway I often use backed enums to make them more friendly to serialize, so not included these means many of my enums wouldn't qualify for synthesis. This would be frustrating to me and likely many users of the language. IMO it should be added before this proposal is added to the language.

To sum up:
Yes to what's proposed, please also add support for value backed enums with the same approach, and great job @taylorswift! Looking forward to using this.

1 Like

What is your evaluation of the proposal?

+1

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

Yes

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

Yes

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

No

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

Read proposal. I think the current limitation of raw values makes sense unless there was a way to force the order and values to produce the same synthesis. I think it would be super confusing if raw values diverge from the declaration order. That can be it’s own proposal.

1 Like

It's either my English or not enough :coffee:, but I'm having an issue understanding this long sentence. Can you shortly elaborate what you expect to work and what not. A short example will also work.

Edit:

So if this means that we'll get a default synthetization by the declaration order regardless of an implicit or explicit RawRepresentable conformance with an opt-out option in form of manual implementation of the static func < (which has precedence to Equatable / Hashable synthetization), then I'm +1 on this.

3 Likes

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