I think this shows that we might've made a bad call with diverging the behavior between resilient and non-resilient libraries here. I support adding an attribute to allow for explicitly opting enums into extensible behavior. There could also be an opportunity for us to eliminate the language dialect here in Swift 6, and make it so that the next breaking change of the language always defaults enums to non-frozen behavior in all modules.
I agree with @typesanitizer that there's something interesting to investigate in the idea of version-locked vs. non-version-locked dependencies, though—the choice about whether to version-lock is usually up to the client, not the library vendor. This could also enable more optimization when a client app (i.e. an end product) bundles a module with it that has library evolution enabled—it can make more assumptions because its dependency is bundled. (This has come up before but I'm not sure what terms to search for.)
I do see that there's a gap here, but I'm not convinced that adding an opt-in for extensible enums is the right way to go, as opposed to something like what @Joe_Groff is saying. Maybe it should be a warning to have a missing
@unknown default when the dependency is not resilient, and you can opt out of that warning by saying the dependency is "rev-locked" or "shipped with the product", either through an attribute on the
import or a command line flag passed by SwiftPM or Xcode.
(As to why this wasn't considered during the initial round of SE-0192, that proposal was contentious enough, and did generate a bunch of annoyance from users about what it did start requiring.)
I agree, deciding whether modules are "version-locked" or not sounds like a better basis for the effect we were originally trying to get with the design here than library-evolution vs not. That would still support the "typical app developer" use case, where someone uses modules only for code organization still gets exhaustivity checking for enums for free without added ceremony, while allowing for uniform behavior for libraries that use modules for versioned distribution, whether they do as resilient binaries or source packages.
Presumably this would bring
@frozen to non-resilient libraries, such that developers can say that specific enumerations will never be evolved, regardless of whether the dependencies are version-locked.
I feel like it would make more sense to make this a compiler error. When I use the
-enable-library-evolution compiler parameter when compiling a library, that is me (the user) saying 'I know that an enum could add a new case later'. But when a developer adds
@extensible to an enum, that is the developer telling the user 'hey, I'll probably be adding more cases down the road', and so I think it makes sense to force the user of that library to handle that case, instead of potentially ignoring it and having their code randomly break when they pull a new version of the library.
If you want to argue that in the end, it's the user's fault if they ignore the warning, I'm fine with that also.
Apologies if it was already mentioned and I missed it. Do I understand correctly that this would allow us to fix the core libraries’ issue where they’re not entirely source‐compatible with themselves on different platforms simply because some use library evolution and others don’t. Here are some old threads describing what I’m talking about:
I agree - whether or not an enum in a library will evolve to add cases is a decision for the library (and it makes sense that it should default to yes/maybe). The client may decide that it doesn’t care about new cases in new library versions and regain exhaustive switching.
That really does make the most sense, and I think it’s an overall simpler model for users to understand. It’s also nice that it applies module-wide rather than on a per-enum basis.
The only place where it gets hairy is if we want to allow third-parties to add cases to enums (which IIRC was something we weren’t sure about in the initial proposal). I would say it's not worth supporting that usecase. It would mean that even the declaring module can’t exhaustively switch over its own enum, so we would need some kind of special annotation for it, and if you’re doing that anyway, you could always add a
.custom(Any) case to get the same behaviour.
I know this has its own drawbacks, but couldn’t in the short term we support building SPM packages as resilient libraries with a flag in the Package manifest? Rather than adding another annotation to the languages glossary?
That does not address the problem. I do not want to make my entire library resilient: I just want to be able to add new cases to an enum without causing compile errors in my downstreams. Resilience is a lot more than just adding extensibility to enumerations, and it comes with substantial performance tradeoffs that are rarely worth it for source-available libraries.
In general resilience transforms ABI stability into API stability: in most cases (not all! please be cautious!) an API-compatible change will be an ABI compatible change in a resilient library. What I’m trying to address is that resilient libraries have an extra layer of API compatibility available to them compared to non-resilient ones: they can add enum cases.
Presumably this change is source-breaking. Is it really necessary to wait for Swift 6 to resolve this issue?
Well, it would mean that all uses of enums from other modules would need to handle unknown cases (unifying how resilient/non-resilient enums are handled), or import those modules with a special attribute.
So it would break a lot of code and will require a new
-swift-version. Whether that is 6.0 or 5.4 or whatever isn’t hugely important IMO. It’s just a number, and presumably we would support the old (i.e. current) language version for a year or two, and users can upgrade module-by-module if they want.
+1 from me on this given I was required to write a "fake" enum struct just the other day. They are really cumbersome, especially for enums used as Errors
Right, to add more context: Where we can (no associated values necessary), we switched to this model for errors which we all hate.
This isn't entirely relevant, but it would be nice to allow enumerations to be extended with new cases regardless of whether they are
@frozen from within the same file, or even just the same module.
Just seeing this now, extremely +1!
We are preparing for an upcoming 1.0 release right now and have been combing through our whole API to look out for forward compatibility concerns. We've now switched several types that were modeled very nicely by enums over to structs for exactly this reason
My preference would be to make it a requirement to mark all enums @frozen in resilient libraries in Swift 6+ and make the default everywhere be extendable.
Frozen enums are a major drag for making frameworks that are just used internally by a single app as a way to avoid code tangle in the app by making it modular. The contortions required because extending enums isn't possible in this context are painful.
If this proposal makes that possible I'm +100 (inflation!).
Though (catching up) reading Enum cases as protocol witnesses - #20 by sveinhal , it uses a protocol to define common enum cases which can then be "extended" in concrete enum definitions that conform to the protocol which might actually solve our use case more cleanly.
Is it possible to help move this along?
I think we should also consider making enums which conform to
Error extensible by default (with the ability to opt-out by marking them
@frozen). Obviously this wouldn't handle retroactive conformances and introduces additional complexity to the language, but it would make a more sensible default for error-enums than our current implicit
@frozen. I think the current situation qualifies as being 'actively harmful' and warrants a source-breaking change.
My hesitation with pushing this forward myself has been that @jrose and @Joe_Groff have both given responses that I've had categorised as "tentatively in favour of a different approach". I think for me to be motivated to continue this work would require hashing out whether or not that alternative approach is worth doing. I'm reluctant to invest too much time in a proposal the core team is not going to want to take.
Sorry to revive this after such a long time. I think this is still a pressing issue - I have some libraries that I would like to publish a 1.0 release for, to declare their APIs as stable, but I can't because significant parts of those APIs involve switching over enums and extracting associated values, and the only alternative design (wrapping it in a struct with optional computed properties) is highly inadequate.
Apparently the surface of the moon is +130C in sunlight, and around -110C in shadow (a 240C gradient). That's what these designs feel like, there is just such an extreme difference - the version with enums is elegant and pleasing, and the workaround is just... exceptionally inelegant and displeasing. I'm not really happy exposing this API at all if it can't use an enum, the workaround is that horrible.
The really frustrating part is that Swift has this feature. It just chooses to reserve it for SDK libraries and not make it available whatsoever if your library is a Swift package.
As for alternative approaches, I don't read the responses as rejecting this feature. Quite the opposite:
Version-locked dependencies are an interesting idea, but I don't think they would obsolete this feature; they would simply allow a library's clients to override it. For instance, even if the library author says their enum is non-frozen, since you've pinned its version, you can treat it as frozen regardless. A client with a version-locked dependency is immune to any library evolution considerations at all (because they locked it -- the concept of "versions" no longer apply to them).
Other dependencies - those without version locking - rely on source stability through concepts such as semantic versioning. They would still have this problem that some enums should evolve without breaking source, and some should not, and it isn't clear which evolution strategy applies to which enums.
Unless we are going to deprecate semantic versioning and require all dependencies be locked to a specific revision (and basically eliminate the package ecosystem in the process), we will still need a way to mark which enums may/may not evolve without breaking source.
It has been two years with no advancement on this front (because of advancements on many other fronts). I still think
@nonfrozen is clunky, but it’s also practical and not hard to implement. (I do still think changing the default but only for enums would be too much churn; if we’re gonna do that, we should think about everything that library evolution mode controls, including places where clients could get more flexibility rather than less. And that’s a big ball of mud.)