[swift-evolution] Enums and Source Compatibility

Matthew Johnson matthew at anandabits.com
Thu Aug 10 08:46:18 CDT 2017


> On Aug 10, 2017, at 7:46 AM, James Froggatt via swift-evolution <swift-evolution at swift.org> wrote:
> 
> Since it seems to have been lost in the noise, I want to second with support for Xiaodi's syntax of having `default` appearing in the enum declaration itself.
> 
> It's much clearer in its intention, feels very ‘Swifty’, and more importantly it doesn't prompt whole threads debating the semantics of `open` vs `public`.

I think Xiaodi’s syntax is very elegant if we want to avoid the access control style syntax.  However, it does one problem: the “error of omission” (not thinking about open vs closed) leaves a library author with a closed enum, preventing them from adding cases in the future without breaking compatibility.  I’m not sure this is acceptable.

> 
> ------------ Begin Message ------------ 
> Group: gmane.comp.lang.swift.evolution 
> MsgID: <CAGY80u=kVQA1q=5TMxXxFgM4tLGFUQh61EN1daepEMAA_FoE9Q at mail.gmail.com> 
> 
> On Tue, Aug 8, 2017 at 5:27 PM, Jordan Rose via swift-evolution <
> swift-evolution-m3FHrko0VLzYtjvyW6yDsg at public.gmane.org> wrote:
> 
>> Hi, everyone. Now that Swift 5 is starting up, I'd like to circle back to
>> an issue that's been around for a while: the source compatibility of enums.
>> Today, it's an error to switch over an enum without handling all the cases,
>> but this breaks down in a number of ways:
>> 
>> - A C enum may have "private cases" that aren't defined inside the
>> original enum declaration, and there's no way to detect these in a switch
>> without dropping down to the rawValue.
>> - For the same reason, the compiler-synthesized 'init(rawValue:)' on an
>> imported enum never produces 'nil', because who knows how anyone's using C
>> enums anyway?
>> - Adding a new case to a *Swift* enum in a library breaks any client code
>> that was trying to switch over it.
>> 
>> (This list might sound familiar, and that's because it's from a message of
>> mine on a thread started by Matthew Johnson back in February called
>> "[Pitch] consistent public access modifiers". Most of the rest of this
>> email is going to go the same way, because we still need to make progress
>> here.)
>> 
>> At the same time, we really like our exhaustive switches, especially over
>> enums we define ourselves. And there's a performance side to this whole
>> thing too; if all cases of an enum are known, it can be passed around much
>> more efficiently than if it might suddenly grow a new case containing a
>> struct with 5000 Strings in it.
>> 
>> 
>> *Behavior*
>> 
>> I think there's certain behavior that is probably not *terribly*
>> controversial:
>> 
>> - When enums are imported from Apple frameworks, they should always
>> require a default case, except for a few exceptions like NSRectEdge. (It's
>> Apple's job to handle this and get it right, but if we get it wrong with an
>> imported enum there's still the workaround of dropping down to the raw
>> value.)
>> - When I define Swift enums in the current framework, there's obviously no
>> compatibility issues; we should allow exhaustive switches.
>> 
>> Everything else falls somewhere in the middle, both for enums defined in
>> Objective-C:
>> 
>> - If I define an Objective-C enum in the current framework, should it
>> allow exhaustive switching, because there are no compatibility issues, or
>> not, because there could still be private cases defined in a .m file?
>> - If there's an Objective-C enum in *another* framework (that I built
>> locally with Xcode, Carthage, CocoaPods, SwiftPM, etc.), should it allow
>> exhaustive switching, because there are no *binary* compatibility issues,
>> or not, because there may be *source* compatibility issues? We'd really
>> like adding a new enum case to *not* be a breaking change even at the
>> source level.
>> - If there's an Objective-C enum coming in through a bridging header,
>> should it allow exhaustive switching, because I might have defined it
>> myself, or not, because it might be non-modular content I've used the
>> bridging header to import?
>> 
>> And in Swift:
>> 
>> - If there's a Swift enum in another framework I built locally, should it
>> allow exhaustive switching, because there are no binary compatibility
>> issues, or not, because there may be source compatibility issues? Again,
>> we'd really like adding a new enum case to *not* be a breaking change
>> even at the source level.
>> 
>> Let's now flip this to the other side of the equation. I've been talking
>> about us disallowing exhaustive switching, i.e. "if the enum might grow new
>> cases you must have a 'default' in a switch". In previous (in-person)
>> discussions about this feature, it's been pointed out that the code in an
>> otherwise-fully-covered switch is, by definition, unreachable, and
>> therefore untestable. This also isn't a desirable situation to be in, but
>> it's mitigated somewhat by the fact that there probably aren't many
>> framework enums you should exhaustively switch over anyway. (Think about
>> Apple's frameworks again.) I don't have a great answer, though.
>> 
>> For people who like exhaustive switches, we thought about adding a new
>> kind of 'default'—let's call it 'unknownCase' just to be able to talk about
>> it. This lets you get warnings when you update to a new SDK, but is even
>> more likely to be untested code. We didn't think this was worth the
>> complexity.
>> 
>> 
>> *Terminology*
>> 
>> The "Library Evolution
>> <http://jrose-apple.github.io/swift-library-evolution/>" doc (mostly
>> written by me) originally called these "open" and "closed" enums ("requires
>> a default" and "allows exhaustive switching", respectively), but this
>> predated the use of 'open' to describe classes and class members. Matthew's
>> original thread did suggest using 'open' for enums as well, but I argued
>> against that, for a few reasons:
>> 
>> - For classes, "open" and "non-open" restrict what the *client* can do.
>> For enums, it's more about providing the client with additional
>> guarantees—and "non-open" is the one with more guarantees.
>> - The "safe" default is backwards: a merely-public class can be made
>> 'open', while an 'open' class cannot be made non-open. Conversely, an
>> "open" enum can be made "closed" (making default cases unnecessary), but a
>> "closed" enum cannot be made "open".
>> 
>> That said, Clang now has an 'enum_extensibility' attribute that does take
>> 'open' or 'closed' as an argument.
>> 
>> On Matthew's thread, a few other possible names came up, though mostly
>> only for the "closed" case:
>> 
>> - 'final': has the right meaning abstractly, but again it behaves
>> differently than 'final' on a class, which is a restriction on code
>> elsewhere in the same module.
>> - 'locked': reasonable, but not a standard term, and could get confused
>> with the concurrency concept
>> - 'exhaustive': matches how we've been explaining it (with an "exhaustive
>> switch"), but it's not exactly the *enum* that's exhaustive, and it's a
>> long keyword to actually write in source.
>> 
>> - 'extensible': matches the Clang attribute, but also long
>> 
>> 
>> I don't have better names than "open" and "closed", so I'll continue using
>> them below even though I avoided them above. But I would *really like to
>> find some*.
>> 
>> 
>> *Proposal*
>> 
>> Just to have something to work off of, I propose the following:
>> 
>> 1. All enums (NS_ENUMs) imported from Objective-C are "open" unless they
>> are declared "non-open" in some way (likely using the enum_extensibility
>> attribute mentioned above).
>> 2. All public Swift enums in modules compiled "with resilience" (still to
>> be designed) have the option to be either "open" or "closed". This only
>> applies to libraries not distributed with an app, where binary
>> compatibility is a concern.
>> 3. All public Swift enums in modules compiled from source have the option
>> to be either "open" or "closed".
>> 4. In Swift 5 mode, a public enum should be *required* to declare if it
>> is "open" or "closed", so that it's a conscious decision on the part of the
>> library author. (I'm assuming we'll have a "Swift 4 compatibility mode"
>> next year that would leave unannotated enums as "closed".)
>> 5. None of this affects non-public enums.
>> 
>> (4) is the controversial one, I expect. "Open" enums are by far the common
>> case in Apple's frameworks, but that may be less true in Swift.
>> 
>> 
>> *Why now?*
>> 
>> Source compatibility was a big issue in Swift 4, and will continue to be
>> an important requirement going into Swift 5. But this also has an impact on
>> the ABI: if an enum is "closed", it can be accessed more efficiently by a
>> client. We don't *have* to do this before ABI stability—we could access
>> all enums the slow way if the library cares about binary compatibility, and
>> add another attribute for this distinction later—but it would be nice™ (an
>> easy model for developers to understand) if "open" vs. "closed" was also
>> the primary distinction between "indirect access" vs. "direct access".
>> 
>> I've written quite enough at this point. Looking forward to feedback!
>> Jordan
>> 
> 
> Jordan, I'm glad you're bringing this back up. I think it's clear that
> there's appetite for some forward movement in this area.
> 
> With respect to syntax--which the conversation in this thread has tackled
> first--I agree with the discussion that "open" and "closed" are attractive
> but also potentially confusing. As discussed in earlier threads, both
> "open" and "closed" will constrain the enum author and/or user in ways
> above and beyond "public" currently does, but the terminology does not
> necessarily reflect that (as open is the antonym of closed); moreover, the
> implications of using these keywords with enums don't necessarily parallel
> the implications of using them with classes (for example, an open class can
> be subclassed; an open enum that gains additional cases is, if anything,
> something of a supertype of the original).
> 
> I'd like to suggest a different direction for syntax; I'm putting it
> forward because I think the spelling itself naturally suggests a design as
> to which enums are (as you call it) "open" or "closed," and how to migrate
> existing enums:
> 
> ```
> enum MyClosedEnum {
>  case a
>  case b
>  case c
> }
> 
> enum MyOpenEnum {
>  case a
>  case b
>  case c
>  default
> }
> ```
> 
> In words, an enum that may have future cases will "leave room" for them by
> using the keyword `default`, sort of paralleling its use in a switch
> statement. All existing Swift enums can therefore continue to be switched
> over exhaustively; that is, this would be an additive, source-compatible
> change. For simplicity, we can leave the rules consistent for non-public
> and public enums; or, we could prohibit non-public enums from using the
> keyword `default` in the manner shown above. Obj-C enums would be imported
> as though they declare `default` unless some attribute like
> `enum_extensibility` is used to annotate them.
> 
> Thoughts?
> 
> 
> 
> ------------- End Message ------------- 
> 
> _______________________________________________
> swift-evolution mailing list
> swift-evolution at swift.org
> https://lists.swift.org/mailman/listinfo/swift-evolution



More information about the swift-evolution mailing list