Unlikely-to-suceed pitch: remove implicit Hashable synthesis for enums without associated values

I think there is more value in being consistent about this than there is in minimizing source churn. The migrator can add explicit opt-in conformances where necessary (which will still be synthesized).

1 Like

I'm suggesting that enum Foo: Int { case x = 1, y, z } remain Equatable.

If the most consistent model for that would be notionally a conditional conformance extension AnyEnum /* cannot actually be expressed in Swift today */ : Equatable where Self : RawRepresentable, RawValue : Equatable, then let the chips fall where they may.

I don't find an idiosyncratic, inexpressible model to be the most consistent with the rest of the langue.

: Int and : String is syntactic sugar for synthesizing RawRepresentable. I think it would be much more reasonable to just add Equatable and Hashable to the synthesis that syntax opts into. The cool thing about this approach is that it does not require RawValue to be Hashable or Equatable in order for the enum to get those conformances. The raw value is not a part of the representation of the enum value internally and need not be used for implementing Equatable and Hashable.

2 Likes

The principal value of this feature was in progressive disclosure. I see it much in the same category as the implicitly synthesized initializers. Without it, there's not much one can do with an enum as a beginner: "opt-in conformance" implies that the user understands protocols and conformances, which in most pedagogical approaches is considered a more advanced concept than enums.

Consider the status quo with synthesized initializers, where they are internal only and have no effect on API. If internal-only protocol conformances were possible, then it would make sense to preserve this feature of enums as though it were spelled enum Foo: internal Equatable, internal Hashable. It would avoid both the code size cost for public types and would avoid breaking resilience. Since (if?) we can't do that, taking a similar approach as was done with frozen enums, as Joe suggests, seems like the next best thing.

Sure, I can buy into that approach too.

If we wanted to minimize source churn, we could take a similar approach to what we did with frozen enums—no-payload enums remain implicitly hashable in "source-only" libraries, but must be made explicitly Equatable or Hashable in resilient libraries.

Yeah, I think this would be reasonable.

Another way of looking at this (in the long run — we don't have this concept right now) is that the conformance is internal in a resilient library and must be made explicitly public to be usable outside of it.

3 Likes

There has been a former discussion on this topic: [Proposal] Explicit Synthetic Behaviour.

I was neutral, but mildly annoyed by automatic Equatable/Hashable synthesis for enums:

I had no idea, +1 on removing it.

This is one of those things I wish I had proposed alongside SE-0185, but I didn't potentially want the whole proposal to get bogged down with a source-breaking change.

You had me at consistency and code-size reduction, but resilience is the one that pushes this completely over the finish line for me. Between Codable, Equatable, Hashable, and CaseIterable, we've continued to enforce a belief that synthesized behavior like this should be opt-in precisely for reasons like that, and I think it's absolutely worth the source break to make sure that we don't paint ourselves into a corner based on legacy behavior that, if we had it to do all over again with the knowledge we have now, would probably go a different way.

9 Likes

I’m on board with removing the implicit synthesis for public enums, but I think it has value and should remain for internal (and below) enums.

The rationale regarding public enums is straightforward: we have a philosophy in Swift that nothing is made public by default, so if the conformance is not spelled out in source code then it should not exist publicly. Plus the active harm regarding code size and resilience, as mentioned by Slava in the original post.

As for internal (etc.) enums, there are several points in favor of retaining the current behavior:

Universality
The only thing you can do with a simple enum is check two instances for equality. Sure, you might pass the instances to a function which does the checking, or you might store them in a collection (eg. Set), but at the end of the day they’re going to get compared.

Boilerplate
It would be needless boilerplate to require adding Hashable conformance to every simple enum just to use it in the only ways it can ever possibly be used.

Convenience
It would be inconvenient to require a switch for every enum comparison, when an if or guard would be better suited sometimes. Not to mention loop conditions and pattern-matching where clauses, which can’t use switch at all.

Progressive disclosure
New developers (or those new to Swift) just want to try thing out and see how it works. They should be able to create and use simple enums in the obvious way without having to learn about protocol conformances first.

Source compatibility
We already have this feature, and it is not actively harmful for non-public enums, so the bar for removing it from them is not met.

4 Likes

This is misleading, you can pattern match an enum just fine without relying on Equatable or Hashable conformance. I expect a very large portion of all user-defined enums to not rely on its Hashable conformance.

The if case and guard case syntax doesn't require Equatable, and usage of == could prompt a fixit to add Equatable conformance. I don't think this change would increase the number of pointless switches people write.

I don't think it's worthwhile to differentiate between public and non-public enums, IMO the inconsistency with Codable/CaseIterable would just make things more confusing.

1 Like

+1.

I've explained in numerous occasions to people who are new to Swift. Most of the time they got the impression that enums are automatically equitable everywhere, ran into associated values and got confused.

3 Likes

I was under the impression that directly comparing enum cases was an intrinsic part of an enum. And that comparing two enum cases when the type is known to be an enum would still be fine after this proposal. Is that not the case here?

…and when you pattern-match a simple enum, you find out whether or not it is a certain case—ie. whether it is equal to another instance. Literally all you can do with an instance of a simple enum is store it, pass it to a function, or check if it’s the same as another one. That is my whole point.

I am not talking about how it is spelled in source code, nor how it is implemented behind the scenes. I am talking about the fact that a simple enum is a set of mutually-exclusive values, and the only thing that can possibly be done with such a type is to check if two of them are the same.

Even if you define a whole slew of methods on a simple enum, that let you add them or mutate them or whatever, when you get down to brass tacks you’re either going to ignore the value, or you’re going to test which case it equals. Everything else is stamp collecting.

1 Like

This falls under progressive disclosure. The if case syntax is not something a beginner should have to learn.

4 Likes

…although, thinking about it again (and apologies for the triple-post), the code-size concern isn’t really active harm. And if a library publishes a simple enum, why *shouldn’t* clients be able to compare them or store them in a Set?

So perhaps Joe Groff is right, and it should only be resilient enums which are exempt from the implicit synthesis.

3 Likes

I think we should remove it, if not only for consistency. And add Hashable as a fixit in the code migrator on all previously implicitly-conforming types.

To me there doesn’t seem any good reason to keep this around. If you make your own type, you need to specify what capabilities it has. It’s not like writing : Hashable on an enum is conceptually harder than writing it on your own class definition.

5 Likes

I don't know if it's been mentioned, but could we solve the resilience problem by making such implicit conformances non-public, and therefore only usable within the module that declares the enum?

1 Like

John did mention it, but we'd have to invent non-public conformances first.

5 Likes

Yesplease :grin:

5 Likes