SE-0266 — Synthesized Comparable conformance for enum types

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

This is the strongest argument I've seen so far, but it comes with the memberwise initializer exception you note. public synthesized member wise initializers have frequently requested. My latest pitch on this topic was well received.

What do you mean by this? Enum cases can be reordered without breaking source and associated values cannot, so your meaning isn't entirely clear.

You're right—I should have also added reordering cases already has situations today which would result in behavioral changes would change that are similar to what would happen with Comparable synthesis, but for structs, this would be introducing an entirely new source order sensitivity that does not manifest in public API or behavior:

  • Reordering the cases of an enum with synthesized CaseIterable changes its iteration order
  • Reordering the cases of an integer raw value enum where values are inferred changes the raw value

Taken together with the source/API compatibility constraints, I think that's a reasonable guide to omit struct synthesis, at least for now.

1 Like

There seems to be a significant miscommunication here.

We want Comparable synthesis for enums because it is extremely convenient and generally useful, and the source-order definition is obviously correct for a large number of enums.

In contrast, we do not want Comparable synthesis for structs, because it would almost never be correct. Any argument related to synthesizing Comparable for structs is prima facie irrelevant, because doing so is undesirable.

• • •

My point is that, because we want both Equatable and Comparable synthesis for enums, and one refines the other, they should have analogous behaviors.

• • •

Since we all agree that comparing different enum cases should use source order, and comparing the same case with a single associated value should compare that value, the only remaining issue is comparing the same case when it has multiple associated values.

In that situation, the associated values look and feel just like tuples. There is a flat list of values in sequential order, and Swift has already decided how to compare such lists: lexicographically.

I see no benefit to preventing synthesis of Comparable here. If lexicographical comparison is desired, the synthesis brings a massive benefit in both convenience and code maintainability. And if some other order is preferred, the programmer will have to implement it manually regardless.

6 Likes
  • What is your evaluation of the proposal?

Sounds very reasonable. The other day, I was surprised that this didn't exist already.

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

It removes a paper cut without any major drawbacks imo.

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

I think so.

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

It feels similar to Rust and Haskell's deriving.

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

I read it fully.


If I may make a slightly off-topic remark (maybe it should be a different proposal altogether if people think it'd be useful):

I don't know what the Swift culture is around newtype wrappers (afaict, there isn't one) but it might be worthwhile to consider also adding a standard newtype wrapper for equivalence which can be used to ignore particular cases.

Something like (I am probably messing up the syntax somewhere...)

struct AllEquivalent<T> : Comparable {
  field: T
}

func < <T>(lhs: AllEquivalent<T>, rhs: AllEquivalent<T>) -> Bool {
    return false
}

func == <T>(lhs: AllEquivalent<T>, rhs: AllEquivalent<T>) -> Bool {
    return true
}

enum Example : Comparable { // Works
  case fn(AllEquivalent<(Int) -> Int>)
  case pt(Int, Int)
}

+1 from me. I can see how for some it could be confusing where the sort order induced by implicitly assigned String rawValue differs from the sort order induced by this synthesis. But I think of enums as intrinsically int-like, and this seems a sensible and logical addition. It will still need enabling by adding Comparable conformance, so no-one should be surprised by it.

Regarding the struct vs enum debate, it would probably help to give some details of the core team's discussions on this.

When this proposal was originally created, it was just covering enums without associated values. The core team was supportive of synthesizing Comparable to match Equatable and Hashable but felt the use case for enums that weren't raw representable but also had no associated values was fairly small, and that it may even be frustrating to add it but not extend it to ones with associated values, which is a much more common use case (just for defining collection indices alone it will be a big help). So we asked @taylorswift to alter the proposal to cover this prior to kicking off the review.

We discussed suggesting extending it to structs as well, but felt that would be a little more controversial. (for example, today you can reorder stored properties on a struct without it making a semantic difference, but this is not true of associated values). We felt it would be a natural break in the feature to support enums but not structs. This does not mean we are for or against adding struct support in future, but that would be proposed as a separate proposal. This allows us to make progress on accepting what will be a very beneficial feature now, with room for expansion later.

From a review management point of view, I'd recommend against debating the merits of struct support until that separate proposal, and instead focusing on what's being proposed for enums. IMO the only reason for rejecting this proposal until it supports structs too would be if it is considered actively harmful (as opposed to slightly surprising) that this support isn't available.

15 Likes

Hmm, I guess that's not true for unkeyed Codable things, so maybe we've already broken the seal on that one.

We don't have unkeyed Codable synthesis, do we?

Ha, true. I uncaveat my caveat.

Let me recaveat that caveat. We still have memberwise initializer. :pensive:

1 Like

The memberwise initializer doesn't strike me to be as much of a concern because

  • It's never public API, whereas associated values of cases always are.
  • In a struct, you can reorder the properties but implement your own initializer to list them in the original order so that you don't have to update the original call sites of the formerly synthesized initializer. For enum cases with associated values, you can add your own static factory method that does the same thing to handle construction of values, but you cannot do anything to avoid having to update the pattern binding sites; those still have to be manually updated.

So, reordering the fields in a struct only affects a specific module-internal usage, and even that has an "escape hatch" to localize the effects of that change; enums with associated values do not have the same characteristics.

4 Likes
  • What is your evaluation of the proposal?
    +1, +more if extended to cover raw value enums (compared by decl order)

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

  • 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?
    I haven't used other languages with enums this rich (languages I have used with more shallow enums do tend to support checking order of enums, but via raw values, which default to the same as the decl order)

  • How much effort did you put into your review? A glance, a quick reading, or an in-depth study?
    Full read of the proposal, a quick read of all prior responses on the thread

Thank you (and @allevato) for providing the rationale. It's worth pointing out that you can re-order enum cases today without a semantic difference. So this proposal already introduces source-order semantics that do not exist today. That said, I mostly just wanted to make sure these issues were being carefully considered. That was not evident in the proposal document. The details in this thread including rationale, alternatives and possible future direction of structs should be captured in an update to the document.

I still think we should consider in this review whether the @memberwise annotation included in the differentiable programming manifesto is a good idea or not, and if it is whether it is relevant to this proposal. The rationale is that a memberwise-synthesized conformance "doesn't make sense as a default implementation in all cases". That feels loosely similar to the source-order dependence of this proposal. In both cases, the attribute provides an indication that the programmer needs to carefully consider the semantics of the synthesized implementation.

We have CaseIterable synthesis.

IMO, annotation would be useful if we have another common alternative. If we have only 1 default + exception, we might as well just use synthesis vs providing custom implementation.

1 Like