Extensible Enumerations for Non-Resilient Libraries

You don‘t have to take my post seriously, but it would be kinda cool if the attribute was an antonym of frozen such as hot or unfrozen. :hot_face: Other than that I think it‘s a good idea to support both variants to help library authors.

Yeah, this is a bit awkward. There is no such functionality for doing this today across Swift modules. We could arrange to do so, but I’d want to do so holistically across both resilient and non-resilient modules, and so I kinda think it’s out of scope for this pitch.

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.

8 Likes

I agree with @Varun_Gandhi 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.)

4 Likes

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.

2 Likes

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.

3 Likes

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.

1 Like

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?

1 Like

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

2 Likes

Right, to add more context: Where we can (no associated values necessary), we switched to this model for errors which we all hate.

2 Likes

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 :cry:

4 Likes

+1
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 , 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.

2 Likes
Terms of Service

Privacy Policy

Cookie Policy