Enums and Source Compatibility

I disagree with the choice of `exhaustive` and `nonexhaustive`. They are too long; the more resilient keyword is longer than the more fragile one (and difficult to read!); and they don't match the clang annotation. We may have to compromise on one or two of these, but the combination of all three ought to be considered disqualifying.

I think `final`/`nonfinal`, `total`/`partial`, `fixed`/? or `permanent`/? are all better because they're shorter, although they all have problems with their antonyms. `candid`/`coy` or `candid`/`shy` produce the right soft default, but are kind of weirdly figurative.

But I don't think a change of keywords will fix everything here. Fundamentally, I am not convinced that source compatibility of `switch` statements should be weighed so heavily. Based on your survey of Foundation, you suggest that the vast majority of imported enums should source-break all switches in Swift 5. Why is that acceptable, but making Swift enums source-breaking unacceptable?

I suspect that, in practice, `public` enums tend to fall into two categories:

  1. "Data enums" which represent important data that happens to consist of a set of alternatives. Outside users will frequently need to switch over these, but they are not very likely to evolve or have private cases.

  2. "Mode enums" which tweak the behavior of an API. These are very likely to evolve or have private cases, but outside users are not very likely to need to switch over them.

An example of a data enum would be, as you mentioned, `NSComparisonResult`. People really *do* need to be able to test against it, but barring some fundamental break in the nature of reality, it will only ever have those three cases. So it's fine to make it exhaustive.

An example of a mode enum would be `UIViewAnimationCurve`, which tells UIKit how to ease an animation. I chose that example because I actually traced a bug just last week to my mistaken impression that this enum had no private cases. I was mapping values of this type to their corresponding `UIViewAnimationOptions` values; because there were private cases, this was Objective-C code, and I didn't include sufficiently aggressive assertions, I ended up reading garbage data from memory. But while debugging this, it struck me that this was actually *really weird* code. How often do you, as a developer outside UIKit, need to interpret the value of a type like `UIViewAnimationCurve`? If the compiler suddenly changed the exhaustiveness behavior of `UIViewAnimationCurve`, probably less than 1% of apps would even notice—and the affected code would probably have latent bugs!

Here's my point: Suddenly treating a mode enum as non-exhaustive is *technically* source-breaking, but *people aren't doing things to them that would break*. It is only the data enums that would actually experience source breakage, and we both seem to agree those are relatively uncommon. So I would argue the relatively rare source breaks are acceptable.

Basically, what I would suggest is this:

  1. In Swift 4.1, we should add a permanent `exhaustive`* keyword and a temporary `@nonexhaustive` attribute to Swift. These are no-ops, or maybe `@nonexhaustive` simply silences the "unreachable default case" warning.

  2. In Swift 4.2 (or whatever Swift 5's Swift 4 mode is called), we should warn about any enum which does not have either `exhaustive` or `@nonexhaustive` attached to it, but publishes them as non-exhaustive. `switch` requires a `default` case for any non-exhaustive public enum.

  3. Swift 5 in Swift 5 mode does the same thing, but does *not* warn about the absence of `@nonexhaustive`.

  4. Swift 5 importing Objective-C treats enums as non-exhaustive by default, unless marked with an attribute.

The dummy keywords in Swift 4.1 ensure that developers can write code that works in both a true Swift 4 compiler and a Swift 5 compiler in Swift 4 mode. (If we don't like that approach, though, we can bump the versions—give Swift 4.2 the behavior I described for Swift 4, give Swift 5 the behavior I described for 4.2, and plan to give Swift 6 the behavior I described for Swift 5.)

* I'm still not super-happy with `exhaustive`, but since `@nonexhaustive` is temporary in this scheme, that at least improves one of the complaints about it. I think the keywords I discussed above would still be improvements.

  * * *

But let's explore an entirely different design. This is a little bit loose; I haven't thought it through totally rigorously.

`SKPaymentTransactionState`, which tells you the status of an in-app purchase transaction, probably would have seemed like a data enum in iOS 3. After all, what states could a transaction take besides `purchasing`, `purchased`, `failed`, or `restored`? But in iOS 8, StoreKit introduced the `deferred` state to handle a new parental-approval feature. Third-party developers did not expect this and had to scramble to handle the unanticipated change.

The frameworks teams often solve this kind of issue by checking the linked SDK version and falling back to compatible behavior in older versions. I don't think StoreKit did this here, but it seems to me that they could have, either by returning the `purchasing` state (which at worst would have stopped users from doing anything else with the app until the purchase was approved or declined) or by returning a `failed` state and then restoring the purchase if it was later approved. At worst, if they had trapped when an incompatible app had a purchase in the `deferred` state, developers might have fixed their bugs more quickly.

I think we could imagine a similar solution being part of our resilience system: Frameworks can add new cases to an enum, but they have to specify compatibility behavior for old `switch` statements. Here's an example design:

A `public enum` may specify the `switch` keyword in its body. (I'm not 100% happy with this keyword, but let's use it for now.) If it does, then the enum is exhaustive:

  // A hypothetical pure-Swift version of `SKPaymentTransaction`.
  @available(iOS 3.0)
  public enum PaymentTransactionState {
    case purchasing
    case purchased(Purchase)
    case restored(Purchase)
    case error(Error)
    
    switch
  }

If it later adds an additional case, or it has non-public cases, it must add a block after the `switch` keyword. The block is called only if `self` is of a case that the calling code doesn't know about; it must either return a value that the caller *does* know about, or trap. So if we added `deferred`, we might instead have:

  @available(iOS 3.0)
  public enum PaymentTransactionState {
    case purchasing
    case purchased(Purchase)
    case restored(Purchase)
    case error(Error)

    @available(iOS 8.0)
    case deferred
    
    switch {
      return .purchasing
    }
  }

(The same logic is applied to the value returned by the block, so if iOS 12 added another case, it could fall back to `deferred`, which would fall back to `purchasing`.)

The `switch` keyword may be followed by a return type; public callers will then need to write their `case` statements as though they were matching against this type. So if, back in iOS 3, you had said this:

  @available(iOS 3.0)
  public enum PaymentTransactionState {
    case purchasing
    case purchased(Purchase)
    case restored(Purchase)
    case error(Error)
    
    switch -> PaymentTransactionState?
  }

Then every `switch` statement on a `PaymentTransactionState` would have had to be written like:

  switch transaction.state {
  case .purchasing?:
    …
  case .purchased?:
    …
  case .restored?:
    …
  case .error?:
    …
  case nil:
    // Handle unexpected states
  }

And then when you added a new case in iOS 8, you could say this, and everyone's code would run through the `nil` path:

  @available(iOS 3.0)
  public enum PaymentTransactionState {
    case purchasing
    case purchased(Purchase)
    case restored(Purchase)
    case error(Error)

    @available(iOS 8.0)
    case deferred
    
    switch -> PaymentTransactionState? {
      return nil
    }
  }

An alternative design would have been to add a `case other` from the start, anticipating that future versions would need to map unknown cases to that one. (Or you could specify `switch -> Never` to forbid switching entirely, or perhaps we could let you say `switch throws` to require the user to say `try switch`. But you get the idea.)

Finally, the kicker: If you do *not* specify an `exhaustive` block, then it is treated as though you had written `switch -> Self? { return nil }`. That is, a "non-exhaustive" enum is just one which turns into an optional when you switch over it, and returns `nil` for unknown cases. Thus, there basically *are* no unknown cases.

Implementation-wise, I imagine that when switching over an enum from `public`, you'd need to make a call which took a version parameter and returned a value compatible with that version. (This might need to be some sort of table of versions, depending on how we end up extending @available to support versions for arbitrary modules.)

  * * *

As for the "untestable code path" problem…maybe we could let you mark certain enum parameters as `@testable`, and then, when brought in through an `@testable import`, allow a `#invalid` value to be passed to those parameters.

  // Library code
  extension PurchasableItem {
    func updateInventory(for state: @testable PaymentTransactionState, quantity: Int) throws {
      switch state {
      case .purchasing:
        return
      case .purchased, .restored:
        inventory += quantity
      case .failed(let error):
        throw error
      default:
        throw ProductError.unknownTransactionState
      }
    }
  }

  // Test
  func testUnknownTransactionState() {
    XCTAssertThrowsError(myProduct.update(for: .#invalid) { error in
      XCTAssertEqual(error, ProductError.unknownTransactionState)
    }
  }

An `@testable` value could not be passed to a non-`@testable` parameter or into a non-`@testable` module, including the actual module the original type came from, unless you had somehow excluded the possibility of an `#invalid` value. You would need to design your code rather carefully to work around this constraint, but I think it could be done.

···

On Sep 5, 2017, at 5:19 PM, Jordan Rose via swift-evolution <swift-evolution@swift.org> wrote:

I've taken everyone's feedback into consideration and written this up as a proposal: https://github.com/jrose-apple/swift-evolution/blob/non-exhaustive-enums/proposals/nnnn-non-exhaustive-enums.md\. The next step is working on an implementation, but if people have further pre-review comments I'd be happy to hear them.

--
Brent Royal-Gordon
Architechies

Hi Jordan,

The proposal looks very reasonable to me.

I don’t have too strong an opinion on this topic, but it occurred to me that your discussion of `future` left out one possible design approach. We could restrict code in the `future` clause to be `break` or `fatalError()`. One of these is probably what most people would do any way and neither really requires testing. As you noted, it will be possible to have untestable code in a `default` clause which is probably what people asking for `future` will have in the relevant switch statements.

Supporting `future` does seem like a nice way to allow libraries to add cases without a breaking change while allowing users to opt-in to a breaking change when that happens. It’s a nice compromise that doesn’t appear to harm anyone.

The main argument against it is: what are the use cases? I haven’t thought enough about it to answer that question. I would challenge people asking for `future` to try to provide some concrete examples, probably referencing enums in Apple frameworks. Maybe if sufficient motivation can be demonstrated we should reconsider the more limited form of `future` that doesn’t involve untestable code. On the other hand, `future` is something that can always be added later.

As I said, I don’t have a strong opinion about this either way at the moment.

Matthew

···

On Sep 5, 2017, at 7:19 PM, Jordan Rose <jordan_rose@apple.com> wrote:

I've taken everyone's feedback into consideration and written this up as a proposal: https://github.com/jrose-apple/swift-evolution/blob/non-exhaustive-enums/proposals/nnnn-non-exhaustive-enums.md\. The next step is working on an implementation, but if people have further pre-review comments I'd be happy to hear them.

Jordan

On Aug 8, 2017, at 15:27, Jordan Rose <jordan_rose@apple.com <mailto:jordan_rose@apple.com>> 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/&gt;&quot; 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

Hi Jordan,

I’m not sure how much bearing on this my comment will have.

Have you considered having only “exhaustive” as a keyword, and make the default non-exhaustive? It seems that “exhaustive" would be the rarer case, as it promises a lot more about compatibility (much like there is no such thing as “non-final”). Also, non exhaustive seems a massive mouthful despite it probably being the correct term.

- Rod

···

On 6 Sep 2017, at 10:19 am, Jordan Rose <jordan_rose@apple.com> wrote:

I've taken everyone's feedback into consideration and written this up as a proposal: https://github.com/jrose-apple/swift-evolution/blob/non-exhaustive-enums/proposals/nnnn-non-exhaustive-enums.md\. The next step is working on an implementation, but if people have further pre-review comments I'd be happy to hear them.

Jordan

On Aug 8, 2017, at 15:27, Jordan Rose <jordan_rose@apple.com <mailto:jordan_rose@apple.com>> 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/&gt;&quot; 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

Hi Jordan,

I apologize in advance that I haven’t followed the back and forth on this thread, so I’m sorry if these thoughts are duplicative:

I really would prefer to avoid introducing the notion of exhaustive/nonexhaustive enums into Swift, and would much prefer that such a thing be limited to C (which can’t express this concept already).

We’ve talked about enums many times across years, and it seems like the appropriate model follows the generally understood resilience model. Specifically, there should be three different kinds of enums, and the kind should affect users outside their module in different ways:

1. private/fileprivate/internal enum: cases can be added freely. All clients are in the same module, so the enum is implicitly fragile, and all switches within the current module may therefore be exhaustive.

2. public enum (i.e., one that isn’t marked fragile): cases may be added freely. Within the module that defines the enum, switches may be exhaustive. However, because the enum is public and non-fragile, clients outside the current module must be prepared for the enum to add additional cases in future revisions of the API, and therefore they cannot exhaustively match the cases of the enum.

3. fragile public enum: cases may not be added, because that would break the fragility guarantee. As such, clients within or outside of hte current module may exhaustively match against the enum.

This approach gives a very natural user model: app developers don’t have to care about enum resilience until they mark an enum as public, and even then they only have to care about it when/if they mark an enum as public. This also builds on the notion of fragility - something we need for other nominal types like structs and classes - so it doesn’t introduce new language complexity. Also such an approach is entirely source compatible with Swift 3/4, which require defaults (this isn’t an accident, it follows from the anticipated design).

This approach doesn’t address the problem of what to do with C though, because C doesn’t have a reasonable notion of “extensible” vs “nonextensible” enum. As such, we definitely do need an attribute (or something) to add to Clang. I think that your proposal for defaulting to “extensible” and using __attribute__((enum_extensibility(closed))) override this is perfectly sensible.

-Chris

···

On Sep 5, 2017, at 5:19 PM, Jordan Rose via swift-evolution <swift-evolution@swift.org> wrote:

I've taken everyone's feedback into consideration and written this up as a proposal: https://github.com/jrose-apple/swift-evolution/blob/non-exhaustive-enums/proposals/nnnn-non-exhaustive-enums.md\. The next step is working on an implementation, but if people have further pre-review comments I'd be happy to hear them.

I disagree. Closed is indeed the stronger guarantee, but APIs are
designed differently in Swift; closed is a sensible default. We
shouldn’t need to define new keywords and increase the surface area of
the language for something that has verisimilitude with the existing
open syntax.
Sincerely,
  Zachary Waldowski
  zach@waldowski.me

···

On Wed, Aug 9, 2017, at 06:23 AM, David Hart via swift-evolution wrote:>

On 9 Aug 2017, at 09:21, Adrian Zubarev via swift-evolution <swift-
evolution@swift.org> wrote:>> Hi Jordan, is that only me or haven't you metioned the default should

be applied to all new enums? Personally I'd say that 'closed' should
be the default and the 'open' enum would require an extra keyword.>

I think it should definitely be the other way round for public enums
because closed is the stronger guarantee. Final is the default for
classes because open is the stronger guarantee. That’s probably why we
should not use the same keywords.>

Now about the keyword itself. Here are two keywords that IMHO nail
their behavior down to the point:>>
finite enum A {} - so to say a closed enum (default)
infinite enum B {} - so to say an open enum (requires default case in
a switch statement)>>
If you think the default should be the other way around, than feel
free to switch that. 'finite' also implies that the enum connot ever
be extended with more cases (to become infinite), which was also
mentioned in your email.>>
--
Adrian Zubarev
Sent with Airmail
Am 9. August 2017 um 00:27:53, Jordan Rose via swift-evolution (swift-
evolution@swift.org) schrieb:>>>

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[1]" 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
_______________________________________________
swift-evolution mailing list swift-evolution@swift.org
https://lists.swift.org/mailman/listinfo/swift-evolution&gt;&gt;&gt;

_______________________________________________
swift-evolution mailing list
swift-evolution@swift.org
https://lists.swift.org/mailman/listinfo/swift-evolution

_________________________________________________
swift-evolution mailing list
swift-evolution@swift.org
https://lists.swift.org/mailman/listinfo/swift-evolution

Links:

  1. Library Evolution Support in Swift (“Resilience”) — Swift 3.0 documentation

(Now with more mailing lists in the "to" field!)

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" 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!

How does this compare with the other idea (I can't remember who posted it) of allowing enum "subtyping"?
enum Foo {
  case one
  case two
}
enum Bar : Foo {
  // implicitly has Foo's cases, too
  case three
}

That way, if you switch over a `Foo`, you'll only ever have two cases to worry about. Code that needs to handle all three cases would need to switch over a `Bar`, but could also switch over a `Foo` since its cases are a subset of Bar's cases.

It's worth noting here that Foo is a subtype of Bar, not the other way around (which is implied by the syntax), because while it is the case that every instance of Foo is also a Bar, not every instance of Bar is also a Foo.

So, the interesting thing about enums is that if you allow this kind of syntax, it means they can retroactively gain *supertypes*; I don't know enough about type theory to know whether that would be a problem or not. (Maybe it's not much different than retroactive protocol conformance?)

Something like this definitely feels useful for cleanly migrating users away from an old enum to a new one, but we may still struggle with some of the classic covariance problems:

enum Foo {
  case one
  case two
}
// I'm not recommending this syntax, just writing it differently to avoid the subtyping confusion stemming from overloading the colon
enum NewFoo including Foo {
  case three
}

I agree with your observations regarding syntax that matches class inheritance or protocol conformance. The syntax I have played with in the past looks like this:

enum NewFoo {
  cases Foo
  case three
}

This syntax has the advantage of placing all case declarations side by side, including the embedded cases.

Yeah, that's probably better... Should there be a colon, "cases: Foo", to makes it a bit harder to typo your way to adding a ton of cases?

It is also very similar to the closest workaround we have today (although without a formal subtype relationship):

enum NewFoo {
  case foo(Foo)
  case three

  // also a static var or func for each case of Foo used to create values
}

Yeah, I was thinking about this earlier. "Extending" enums could actually be implemented like this behind the scenes (with tons of sugar to flatten out switches), but I think it'd be better to go ahead and synthesize the whole thing, since it seems like it'd be better to go into IRGen with one giant switch instead of a bunch of nested switches (I know compilers sometimes do this as an optimization, but the way they break it up might not be remotely close to the source code breaks them up).

- Dave Sweeris

···

On Aug 9, 2017, at 11:04, Matthew Johnson <matthew@anandabits.com> wrote:

On Aug 9, 2017, at 12:15 PM, Tony Allevato via swift-evolution <swift-evolution@swift.org> wrote:

On Wed, Aug 9, 2017 at 9:40 AM David Sweeris via swift-evolution <swift-evolution@swift.org> wrote:

On Aug 8, 2017, at 3:27 PM, Jordan Rose via swift-evolution <swift-evolution@swift.org> wrote:

(Now with more mailing lists in the "to" field!)

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" 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!

How does this compare with the other idea (I can't remember who posted it) of allowing enum "subtyping"?
enum Foo {
  case one
  case two
}
enum Bar : Foo {
  // implicitly has Foo's cases, too
  case three
}

That way, if you switch over a `Foo`, you'll only ever have two cases to worry about. Code that needs to handle all three cases would need to switch over a `Bar`, but could also switch over a `Foo` since its cases are a subset of Bar's cases.

It's worth noting here that Foo is a subtype of Bar, not the other way around (which is implied by the syntax), because while it is the case that every instance of Foo is also a Bar, not every instance of Bar is also a Foo.

So, the interesting thing about enums is that if you allow this kind of syntax, it means they can retroactively gain *supertypes*; I don't know enough about type theory to know whether that would be a problem or not. (Maybe it's not much different than retroactive protocol conformance?)

Something like this definitely feels useful for cleanly migrating users away from an old enum to a new one, but we may still struggle with some of the classic covariance problems:

enum Foo {
  case one
  case two
}
// I'm not recommending this syntax, just writing it differently to avoid the subtyping confusion stemming from overloading the colon
enum NewFoo including Foo {
  case three
}

I agree with your observations regarding syntax that matches class inheritance or protocol conformance. The syntax I have played with in the past looks like this:

enum NewFoo {
  cases Foo
  case three
}

This syntax has the advantage of placing all case declarations side by side, including the embedded cases. It is also very similar to the closest workaround we have today (although without a formal subtype relationship):

enum NewFoo {
  case foo(Foo)
  case three

  // also a static var or func for each case of Foo used to create values
}

···

Sent from my iPad

On Aug 9, 2017, at 12:15 PM, Tony Allevato via swift-evolution <swift-evolution@swift.org> wrote:

On Wed, Aug 9, 2017 at 9:40 AM David Sweeris via swift-evolution <swift-evolution@swift.org> wrote:

On Aug 8, 2017, at 3:27 PM, Jordan Rose via swift-evolution <swift-evolution@swift.org> wrote:

fooConsumer(_ foo: Foo) can be changed to fooConsumer(_ foo: NewFoo) without breaking clients because the clients would be passing Foos, and any Foo is also a NewFoo.
fooProducer() -> Foo *cannot* be changed to fooProducer() -> NewFoo without breaking clients because the client is expecting a Foo, but not all NewFoos are Foos.

I don't know how libraries would deal with adding cases... maybe have different function signatures based on the version setting?

- Dave Sweeris
_______________________________________________
swift-evolution mailing list
swift-evolution@swift.org
https://lists.swift.org/mailman/listinfo/swift-evolution

_______________________________________________
swift-evolution mailing list
swift-evolution@swift.org
https://lists.swift.org/mailman/listinfo/swift-evolution

(Now with more mailing lists in the "to" field!)

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/&gt;&quot; 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!

How does this compare with the other idea (I can't remember who posted it) of allowing enum "subtyping"?
enum Foo {
  case one
  case two
}
enum Bar : Foo {
  // implicitly has Foo's cases, too
  case three
}

That way, if you switch over a `Foo`, you'll only ever have two cases to worry about. Code that needs to handle all three cases would need to switch over a `Bar`, but could also switch over a `Foo` since its cases are a subset of Bar's cases.

It's worth noting here that Foo is a subtype of Bar, not the other way around (which is implied by the syntax), because while it is the case that every instance of Foo is also a Bar, not every instance of Bar is also a Foo.

So, the interesting thing about enums is that if you allow this kind of syntax, it means they can retroactively gain *supertypes*; I don't know enough about type theory to know whether that would be a problem or not. (Maybe it's not much different than retroactive protocol conformance?)

In one sense, it's just one more implicit conversion kind, and we know how to model that. On the other hand, implicit conversions are a large part of why some Swift expressions take a long time to type-check, and there is a "hierarchy" of sorts because you could overload on Foo and Bar.

Something like this definitely feels useful for cleanly migrating users away from an old enum to a new one, but we may still struggle with some of the classic covariance problems:

enum Foo {
  case one
  case two
}
// I'm not recommending this syntax, just writing it differently to avoid the subtyping confusion stemming from overloading the colon
enum NewFoo including Foo {
  case three
}

fooConsumer(_ foo: Foo) can be changed to fooConsumer(_ foo: NewFoo) without breaking clients because the clients would be passing Foos, and any Foo is also a NewFoo.
fooProducer() -> Foo *cannot* be changed to fooProducer() -> NewFoo without breaking clients because the client is expecting a Foo, but not all NewFoos are Foos.

This is a nice observation, but I'll add that it only works for binary frameworks if the enum layout isn't part of the ABI. I think we'd rather focus on having the flexibility to change cases within an existing enum than migrating to another enum, which works in both cases.

(That doesn't mean enum inclusion couldn't be a useful feature on its own, and I'm not going to argue that one way or another in this effort. Just that it doesn't have a strong effect on source compatibility and library evolution.)

Jordan

···

On Aug 9, 2017, at 10:15, Tony Allevato via swift-evolution <swift-evolution@swift.org> wrote:
On Wed, Aug 9, 2017 at 9:40 AM David Sweeris via swift-evolution <swift-evolution@swift.org <mailto:swift-evolution@swift.org>> wrote:
On Aug 8, 2017, at 3:27 PM, Jordan Rose via swift-evolution <swift-evolution@swift.org <mailto:swift-evolution@swift.org>> wrote:

:-) As you've all noted, there are some conflicting concerns for the default:

- Source compatibility: the existing behavior for an unannotated enum is "closed".
- Intuition: if you show someone an enum without an explicit annotation, they'll probably expect they can switch over it. (I'm going to say this is why Zach calls it a "sensible default".)
- Consistency: switches on an enum in the same module can always be exhaustive, so having it be different across modules is a bit annoying. (But 'public' already acts like this.)

vs.

- Library evolution: the default should promise less, so that you have the opportunity to change it.
- Flexibility: you can emulate an exhaustive switch with a non-exhaustive switch using fatalError, but not the other way around.

All of this is why I suggested it be an explicit annotation in either direction, but Matthew brought up the "keyword soup" problem—if you have to write (say) "public finite enum" and "public infinite enum", but would never write "private finite enum" or "private infinite enum", something is redundant here. Still, I'm uncomfortable with the default case being the one that constrains library authors, so at least for binary frameworks (those compiled "with resilience") I would want that to be explicit. That brings us to one more concern: how different should binary frameworks be from source frameworks?

Jordan

···

On Aug 9, 2017, at 08:19, Zach Waldowski via swift-evolution <swift-evolution@swift.org> wrote:

I disagree. Closed is indeed the stronger guarantee, but APIs are designed differently in Swift; closed is a sensible default. We shouldn’t need to define new keywords and increase the surface area of the language for something that has verisimilitude with the existing open syntax.

Sincerely,
  Zachary Waldowski
  zach@waldowski.me <mailto:zach@waldowski.me>

On Wed, Aug 9, 2017, at 06:23 AM, David Hart via swift-evolution wrote:

On 9 Aug 2017, at 09:21, Adrian Zubarev via swift-evolution <swift-evolution@swift.org <mailto:swift-evolution@swift.org>> wrote:

Hi Jordan, is that only me or haven't you metioned the default should be applied to all new enums? Personally I'd say that 'closed' should be the default and the 'open' enum would require an extra keyword.

I think it should definitely be the other way round for public enums because closed is the stronger guarantee. Final is the default for classes because open is the stronger guarantee. That’s probably why we should not use the same keywords.

Now about the keyword itself. Here are two keywords that IMHO nail their behavior down to the point:

finite enum A {} - so to say a closed enum (default)
infinite enum B {} - so to say an open enum (requires default case in a switch statement)

If you think the default should be the other way around, than feel free to switch that. 'finite' also implies that the enum connot ever be extended with more cases (to become infinite), which was also mentioned in your email.

--
Adrian Zubarev
Sent with Airmail
Am 9. August 2017 um 00:27:53, Jordan Rose via swift-evolution (swift-evolution@swift.org <mailto:swift-evolution@swift.org>) schrieb:

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/&gt;&quot; 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
_______________________________________________
swift-evolution mailing list
swift-evolution@swift.org <mailto:swift-evolution@swift.org>
https://lists.swift.org/mailman/listinfo/swift-evolution

_______________________________________________
swift-evolution mailing list
swift-evolution@swift.org <mailto:swift-evolution@swift.org>
https://lists.swift.org/mailman/listinfo/swift-evolution

_______________________________________________
swift-evolution mailing list
swift-evolution@swift.org <mailto:swift-evolution@swift.org>
https://lists.swift.org/mailman/listinfo/swift-evolution

_______________________________________________
swift-evolution mailing list
swift-evolution@swift.org
https://lists.swift.org/mailman/listinfo/swift-evolution

I agree this should be considered a simple bug. Have you filed a bug report?

Nevin

That behaviour was never explicitly mentioned in SE-0169 but I agree its confusing. But I’m also fairly sure the only window to do anything about it is Swift 4. Everybody is really worn down by those access level discussions.

For illustration, Vladimir is confused that:

private extension Foo {
    func foo() {}
}

is equivalent to:

fileprivate extension Foo {
    func foo() {}
}

making it accessible to another type in the same file:

struct Bar {
    func bar(foo: Foo) {
        foo.foo()
    }
}

Aren't access levels on extensions supposed to define the default access level of the members of the extension?Is this a bug then?

···

On 9 Aug 2017, at 21:18, Vladimir.S via swift-evolution <swift-evolution@swift.org> wrote:

Could someone remind please, was it decided to stick with 'private extension' means actually fileprivate access level for members declared in such extension or this could be discussed for Swift5?

Currently, when private members are visible in type/extensions of that type in the same file, IMO there is no sense to treat 'private extension' as 'fileprivate extension', it is reasonable to group some private members of type into extension without making them fileprivate, and such members can be used from the type/other extensions.

And also this is a huge inconsistency in my opinion: all other access modifiers 'work' as expected for extensions, but only 'private extension' means not what written, very surprising for one who don't expect this.
_______________________________________________
swift-evolution mailing list
swift-evolution@swift.org
https://lists.swift.org/mailman/listinfo/swift-evolution

Hi Jordan,

let's say I'm writing my custom number formatter and I switch over NSNumberFormatterStyle (NumberFormatter.Style in Swift) - the question always is what to do with the default case - it's a value that I am not programmatically counting with. I would personally just put in fatalError with a description that it was passed an unhandled style. Listing all enums in Foundation, I can see using most of them this way.

I personally have most of my switches exhaustive, mainly for the sake of being warned/error'ed when a new case is introduced - I've just done a quick search through my projects and I use default: usually for switching over non-enums (strings, object matching, ints, ...).

Maybe I'm in the minority here... Seemed like a good practice to me - usually the enum doesn't have but a few items on the list and you usually don't handle just 1-2 cases in your switch, which makes the default label save you 1-2 lines of code that can save you from unnnecessarily crashing during runtime...

···

On Aug 10, 2017, at 1:57 AM, Jordan Rose <jordan_rose@apple.com> wrote:

Hi, Charlie. This is fair—if you're switching over an open enum at all, presumably you have a reason for doing so and therefore might want to handle all known cases every time you update your SDK. However, I think in practice that's going to be rare—do you have examples of exhaustive switches on SDK enums that exist in your own app?

(There's an additional piece about how to handle cases with different availability—there's nowhere obvious to write the #available.)

I suspect marking SDK enums "closed" will be much easier than nullability, simply because there are so few of them. Here's some data to that effect: out of all 60 or so NS_ENUMs in Foundation, only 6 of them are ones I would definitely mark "closed":

- NSComparisonResult
- NSKeyValueChange / NSKeyValueSetMutationKind
- NSRectEdge
- NSURLRelationship
- maybe NSCalculationError

There are a few more, like NSURLHandleStatus, where I could see someone wanting to exhaustively switch as well, but the main point is that there is a clear default for public enums, at least in Objective-C, and that even in a large framework it's not too hard to look at all of them.

(Note that NSComparisonResult is technically not part of Foundation; it lives in the ObjectiveC module as /usr/include/objc/NSObject.h.)

Jordan

On Aug 8, 2017, at 21:53, Charlie Monroe <charlie@charliemonroe.net <mailto:charlie@charliemonroe.net>> wrote:

While I agree with the entire idea and would actually use behavior like this in a few instances, I feel that in most cases, you would simply put

default:
  fatalError()

The huge downside of this is that you no longer get warned by the compiler that you are missing a case that was added - a common thing I personally do (and I have a feeling I'm not alone) - add an enum case, build the app, see what broke and fix it - as you get warnings/errors about the switch not being exhaustive. You find this out during runtime (if you're lucky), otherwise your end user.

As you've noted all enums from ObjC would need to be marked with an annotation marking if they are closed - which given the way nullability is still missing in many frameworks out there, I think would take years.

I'd personally expand this proposal by introducing switch! (with the exclamation mark) which would allow to treat open enums as closed. Example:

// Imported from ObjC
open enum NSAlert.Style { ... }

switch! alert.style {
case .warning:
  // ...
case .informational:
  // ...
case .critical:
  // ...
}

The force-switch would implicitely create the default label crashing, logging the rawValue of the enum.

Thoughts?

On Aug 9, 2017, at 12:28 AM, Jordan Rose via swift-evolution <swift-evolution@swift.org <mailto:swift-evolution@swift.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/&gt;&quot; 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
_______________________________________________
swift-evolution mailing list
swift-evolution@swift.org <mailto:swift-evolution@swift.org>
https://lists.swift.org/mailman/listinfo/swift-evolution

I have a proposal for “alternative types,” which can fulfill the “strong typedef” desired feature from C++. Since I changed from a custom initialization interface to RawRepresentable, I can do conditional translation to the new type, i.e. implement subtypes. (It can also do quotient types.) What do you mean by needing to figure out what public/open mean?

···

On Aug 9, 2017, at 3:57 PM, Adrian Zubarev via swift-evolution <swift-evolution@swift.org> wrote:

I’d very much in favour of a consistent access modifiers across the whole language and eliminate exclusive `open`. `open/public` protocols are more than welcome. Plus it’s already has been said that Swift will potentially support subtyping for value type in some future, where we’ll yet again would need to align what `public` and `open` will mean. So I’d appreciate all the steps that could already be made now to align their meaning as much as it’s possible to this moment.


Daryle Walker
Mac, Internet, and Video Game Junkie
darylew AT mac DOT com

It's in the "Alternatives Considered" section. :-) That was my desired design when we started, but feedback convinced me that the break from Swift 4 mode would be too drastic. The same valid code would have a different meaning whether you were writing Swift 4 or Swift 5.

Jordan

···

On Sep 5, 2017, at 17:30, Rod Brown <rodney.brown6@icloud.com> wrote:

Hi Jordan,

I’m not sure how much bearing on this my comment will have.

Have you considered having only “exhaustive” as a keyword, and make the default non-exhaustive? It seems that “exhaustive" would be the rarer case, as it promises a lot more about compatibility (much like there is no such thing as “non-final”). Also, non exhaustive seems a massive mouthful despite it probably being the correct term.

- Rod

On 6 Sep 2017, at 10:19 am, Jordan Rose <jordan_rose@apple.com <mailto:jordan_rose@apple.com>> wrote:

I've taken everyone's feedback into consideration and written this up as a proposal: https://github.com/jrose-apple/swift-evolution/blob/non-exhaustive-enums/proposals/nnnn-non-exhaustive-enums.md\. The next step is working on an implementation, but if people have further pre-review comments I'd be happy to hear them.

Jordan

Hi Jordan,

I like this new direction. But I have Rod’s inverse question: have you considered only having the nonexhaustive keyword? Similar to how non-final doesn't exist because its opposite is the default behaviour. That would also free us from searching for a good pair of keywords and only find one good keyword (extensible, expandable, …) which doesn’t contain a negative.

David.

···

On 6 Sep 2017, at 02:36, Jordan Rose <jordan_rose@apple.com> wrote:

It's in the "Alternatives Considered" section. :-) That was my desired design when we started, but feedback convinced me that the break from Swift 4 mode would be too drastic. The same valid code would have a different meaning whether you were writing Swift 4 or Swift 5.

Jordan

On Sep 5, 2017, at 17:30, Rod Brown <rodney.brown6@icloud.com <mailto:rodney.brown6@icloud.com>> wrote:

Hi Jordan,

I’m not sure how much bearing on this my comment will have.

Have you considered having only “exhaustive” as a keyword, and make the default non-exhaustive? It seems that “exhaustive" would be the rarer case, as it promises a lot more about compatibility (much like there is no such thing as “non-final”). Also, non exhaustive seems a massive mouthful despite it probably being the correct term.

- Rod

On 6 Sep 2017, at 10:19 am, Jordan Rose <jordan_rose@apple.com <mailto:jordan_rose@apple.com>> wrote:

I've taken everyone's feedback into consideration and written this up as a proposal: https://github.com/jrose-apple/swift-evolution/blob/non-exhaustive-enums/proposals/nnnn-non-exhaustive-enums.md\. The next step is working on an implementation, but if people have further pre-review comments I'd be happy to hear them.

Jordan

Ah yes, my eye skipped that alternative for some reason! Sorry.

I’d be concerned that avoiding a default is a fix for a compatibility problem, not a language design decision. If we look back in 5 years and say “why do we need to keep writing nonexhaustive everywhere?”, we’ll have to say “there were compatibility problems with Swift 4-to-5”. That reeks of a language I just want to walk away from. Yuk.

In this case, either way, we’ll need to do some work. So why not let the migrator migrate this code correctly to “exhaustive”, which is the current behaviour? I think a decision where either way we break source compatibility should be done in the interest of language design, not in the short term interest of avoiding confusion.

But that’s just my 2c.

Thanks,

Rod

···

On 6 Sep 2017, at 10:36 am, Jordan Rose <jordan_rose@apple.com> wrote:

It's in the "Alternatives Considered" section. :-) That was my desired design when we started, but feedback convinced me that the break from Swift 4 mode would be too drastic. The same valid code would have a different meaning whether you were writing Swift 4 or Swift 5.

Jordan

On Sep 5, 2017, at 17:30, Rod Brown <rodney.brown6@icloud.com <mailto:rodney.brown6@icloud.com>> wrote:

Hi Jordan,

I’m not sure how much bearing on this my comment will have.

Have you considered having only “exhaustive” as a keyword, and make the default non-exhaustive? It seems that “exhaustive" would be the rarer case, as it promises a lot more about compatibility (much like there is no such thing as “non-final”). Also, non exhaustive seems a massive mouthful despite it probably being the correct term.

- Rod

On 6 Sep 2017, at 10:19 am, Jordan Rose <jordan_rose@apple.com <mailto:jordan_rose@apple.com>> wrote:

I've taken everyone's feedback into consideration and written this up as a proposal: https://github.com/jrose-apple/swift-evolution/blob/non-exhaustive-enums/proposals/nnnn-non-exhaustive-enums.md\. The next step is working on an implementation, but if people have further pre-review comments I'd be happy to hear them.

Jordan

Here is an alternative view. I've been thinking about this and I feel that instead of adding this to an enum why not make RawRepresentable structs a swift construct.

You could declare it like this:

enum struct {
   case a, b, c
}

This would be a struct that acts like an enum but it is open like a RawRepresentable but using the enum case sugar.

···

On Sep 5, 2017, at 5:37 PM, Jordan Rose via swift-evolution <swift-evolution@swift.org> wrote:

It's in the "Alternatives Considered" section. :-) That was my desired design when we started, but feedback convinced me that the break from Swift 4 mode would be too drastic. The same valid code would have a different meaning whether you were writing Swift 4 or Swift 5.

Jordan

On Sep 5, 2017, at 17:30, Rod Brown <rodney.brown6@icloud.com> wrote:

Hi Jordan,

I’m not sure how much bearing on this my comment will have.

Have you considered having only “exhaustive” as a keyword, and make the default non-exhaustive? It seems that “exhaustive" would be the rarer case, as it promises a lot more about compatibility (much like there is no such thing as “non-final”). Also, non exhaustive seems a massive mouthful despite it probably being the correct term.

- Rod

On 6 Sep 2017, at 10:19 am, Jordan Rose <jordan_rose@apple.com> wrote:

I've taken everyone's feedback into consideration and written this up as a proposal: https://github.com/jrose-apple/swift-evolution/blob/non-exhaustive-enums/proposals/nnnn-non-exhaustive-enums.md\. The next step is working on an implementation, but if people have further pre-review comments I'd be happy to hear them.

Jordan

_______________________________________________
swift-evolution mailing list
swift-evolution@swift.org
https://lists.swift.org/mailman/listinfo/swift-evolution

From the perspective of primarily an app developer rather than library author, I'm not a big fan of this change. I find myself in the "unhappy with the loss of compiler warnings" camp -- if I'm switching over every case of an enum, then I almost certainly want to be notified that a new case has been added by way of a compiler error than via the arbitrary runtime behavior I added in the previously-unreachable default case.

I think what you’re really asking for here is the “future” case mentioned in the Alternatives Considered section. I think that Jordan makes a good point that this would result in untestable code, which is bad practice. While the lack of clear highlighting of non-exhaustive cases is undesirable, I think untestable code is a much larger problem here.

Either way we need a way to handle forward compatibility for our code when cases get added to external frameworks, that much is clear. Swift is broken in regards to this, and we need to handle it somehow. I’m hoping you’re not suggesting that we just don’t make this change at all. We need this for forward compatibility for framework development with Swift.

···

On 6 Sep 2017, at 12:05 pm, Jarod Long via swift-evolution <swift-evolution@swift.org> wrote:

This seems like a clear situation where source compatibility is not desired to me. For those who want to maximize compatibility, it is possible to opt into it by adding a default case to an exhaustive switch over a library enum, but the reverse is not true if this change is made as-is. You can't opt into an exhaustive switch for nonexhaustive enums if handling every case is valued over source compatibility.

A secondary concern I have is that this introduces extra complexity that could be confusing for new Swift developers. The current enum exhaustivity rules are consistent and easy to explain, but they become more cumbersome with this added exception that only applies to some enums that specifically only come from outside the current module. If this change is made, I would encourage some effort towards a specific error message when switching over all cases of a nonexhaustive enum without a default case. Rather than the existing "Switch must be exhaustive", I think it would go a long way towards avoiding confusion to say something like "Switch over a nonexhaustive enum must have a default case".

In any case, I don't think these are terrible issues -- I agree with the proposal's statement that switches over nonexhaustive enums are generally uncommon. But if that's true, it feels like the source compatibility motivation is weak, since not much code is affected anyways. Perhaps the benefits from a library author's perspective make this change worth it, but at least for me and my coworkers, it would be an unwelcome change overall.

Jarod

On Sep 5, 2017, 17:19 -0700, Jordan Rose via swift-evolution <swift-evolution@swift.org>, wrote:

I've taken everyone's feedback into consideration and written this up as a proposal: https://github.com/jrose-apple/swift-evolution/blob/non-exhaustive-enums/proposals/nnnn-non-exhaustive-enums.md\. The next step is working on an implementation, but if people have further pre-review comments I'd be happy to hear them.

Jordan

On Aug 8, 2017, at 15:27, Jordan Rose <jordan_rose@apple.com <mailto:jordan_rose@apple.com>> 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/&gt;&quot; 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

_______________________________________________
swift-evolution mailing list
swift-evolution@swift.org
https://lists.swift.org/mailman/listinfo/swift-evolution

Hi Jordan,

I like this new direction. But I have Rod’s inverse question: have you considered only having the nonexhaustive keyword? Similar to how non-final doesn't exist because its opposite is the default behaviour. That would also free us from searching for a good pair of keywords and only find one good keyword (extensible, expandable, …) which doesn’t contain a negative.

I was thinking the same. If exhaustive is the default, and how Swift has always worked, then why have that keyword anyway? I like extensible/expandable but I worry the connotation some might come away with that you could add cases externally, which is not in scope.

···

On 6 Sep 2017, at 4:35 pm, David Hart <david@hartbit.com> wrote:

David.

On 6 Sep 2017, at 02:36, Jordan Rose <jordan_rose@apple.com <mailto:jordan_rose@apple.com>> wrote:

It's in the "Alternatives Considered" section. :-) That was my desired design when we started, but feedback convinced me that the break from Swift 4 mode would be too drastic. The same valid code would have a different meaning whether you were writing Swift 4 or Swift 5.

Jordan

On Sep 5, 2017, at 17:30, Rod Brown <rodney.brown6@icloud.com <mailto:rodney.brown6@icloud.com>> wrote:

Hi Jordan,

I’m not sure how much bearing on this my comment will have.

Have you considered having only “exhaustive” as a keyword, and make the default non-exhaustive? It seems that “exhaustive" would be the rarer case, as it promises a lot more about compatibility (much like there is no such thing as “non-final”). Also, non exhaustive seems a massive mouthful despite it probably being the correct term.

- Rod

On 6 Sep 2017, at 10:19 am, Jordan Rose <jordan_rose@apple.com <mailto:jordan_rose@apple.com>> wrote:

I've taken everyone's feedback into consideration and written this up as a proposal: https://github.com/jrose-apple/swift-evolution/blob/non-exhaustive-enums/proposals/nnnn-non-exhaustive-enums.md\. The next step is working on an implementation, but if people have further pre-review comments I'd be happy to hear them.

Jordan

Hi Brent,

Thanks for the analysis of the issues here. I think you’re right that there are two major types of enums, and that there are a set developers tend to switch on that are usually fixed, where additional cases won’t make sense.

I think there are several issues that I see with these different ideas, and they stem back to a similar source:

1. They are overly complex solutions to a specific problem: inter-library variable enums. We would be destroying some of the awesome simplicity and design characteristics of enums in Swift to deal with an outlying problem.

2. Project enums should always be exhaustive. Your design introduces ‘exhaustive’ and has an end goal of removing ‘@nonexhaustive’ but the fact is that keyword should be the default internal of a project. It seems an odd trajectory change to make purely for public enums.

It would seem much simpler to have a word that denotes that an enum is not considered ‘exhaustive’ in public code, or to force users to annotate their intention of exhaustive vs extensible when they make their enum public, and have intra-project enums remain exhaustive. This then requires a simple addition of a default argument to handle outlier cases you may encounter. Your solutions seem to me to require a large to deal with a relatively narrow use case, radically changing the enum system.

I agree that we could include something as part of @testable to add an additional case to test the edge case. We do have specific language elements for testing, why not here?

- Rod

···

On 6 Sep 2017, at 10:53 pm, Brent Royal-Gordon via swift-evolution <swift-evolution@swift.org> wrote:

On Sep 5, 2017, at 5:19 PM, Jordan Rose via swift-evolution <swift-evolution@swift.org <mailto:swift-evolution@swift.org>> wrote:

I've taken everyone's feedback into consideration and written this up as a proposal: https://github.com/jrose-apple/swift-evolution/blob/non-exhaustive-enums/proposals/nnnn-non-exhaustive-enums.md\. The next step is working on an implementation, but if people have further pre-review comments I'd be happy to hear them.

I disagree with the choice of `exhaustive` and `nonexhaustive`. They are too long; the more resilient keyword is longer than the more fragile one (and difficult to read!); and they don't match the clang annotation. We may have to compromise on one or two of these, but the combination of all three ought to be considered disqualifying.

I think `final`/`nonfinal`, `total`/`partial`, `fixed`/? or `permanent`/? are all better because they're shorter, although they all have problems with their antonyms. `candid`/`coy` or `candid`/`shy` produce the right soft default, but are kind of weirdly figurative.

But I don't think a change of keywords will fix everything here. Fundamentally, I am not convinced that source compatibility of `switch` statements should be weighed so heavily. Based on your survey of Foundation, you suggest that the vast majority of imported enums should source-break all switches in Swift 5. Why is that acceptable, but making Swift enums source-breaking unacceptable?

I suspect that, in practice, `public` enums tend to fall into two categories:

  1. "Data enums" which represent important data that happens to consist of a set of alternatives. Outside users will frequently need to switch over these, but they are not very likely to evolve or have private cases.

  2. "Mode enums" which tweak the behavior of an API. These are very likely to evolve or have private cases, but outside users are not very likely to need to switch over them.

An example of a data enum would be, as you mentioned, `NSComparisonResult`. People really *do* need to be able to test against it, but barring some fundamental break in the nature of reality, it will only ever have those three cases. So it's fine to make it exhaustive.

An example of a mode enum would be `UIViewAnimationCurve`, which tells UIKit how to ease an animation. I chose that example because I actually traced a bug just last week to my mistaken impression that this enum had no private cases. I was mapping values of this type to their corresponding `UIViewAnimationOptions` values; because there were private cases, this was Objective-C code, and I didn't include sufficiently aggressive assertions, I ended up reading garbage data from memory. But while debugging this, it struck me that this was actually *really weird* code. How often do you, as a developer outside UIKit, need to interpret the value of a type like `UIViewAnimationCurve`? If the compiler suddenly changed the exhaustiveness behavior of `UIViewAnimationCurve`, probably less than 1% of apps would even notice—and the affected code would probably have latent bugs!

Here's my point: Suddenly treating a mode enum as non-exhaustive is *technically* source-breaking, but *people aren't doing things to them that would break*. It is only the data enums that would actually experience source breakage, and we both seem to agree those are relatively uncommon. So I would argue the relatively rare source breaks are acceptable.

Basically, what I would suggest is this:

  1. In Swift 4.1, we should add a permanent `exhaustive`* keyword and a temporary `@nonexhaustive` attribute to Swift. These are no-ops, or maybe `@nonexhaustive` simply silences the "unreachable default case" warning.

  2. In Swift 4.2 (or whatever Swift 5's Swift 4 mode is called), we should warn about any enum which does not have either `exhaustive` or `@nonexhaustive` attached to it, but publishes them as non-exhaustive. `switch` requires a `default` case for any non-exhaustive public enum.

  3. Swift 5 in Swift 5 mode does the same thing, but does *not* warn about the absence of `@nonexhaustive`.

  4. Swift 5 importing Objective-C treats enums as non-exhaustive by default, unless marked with an attribute.

The dummy keywords in Swift 4.1 ensure that developers can write code that works in both a true Swift 4 compiler and a Swift 5 compiler in Swift 4 mode. (If we don't like that approach, though, we can bump the versions—give Swift 4.2 the behavior I described for Swift 4, give Swift 5 the behavior I described for 4.2, and plan to give Swift 6 the behavior I described for Swift 5.)

* I'm still not super-happy with `exhaustive`, but since `@nonexhaustive` is temporary in this scheme, that at least improves one of the complaints about it. I think the keywords I discussed above would still be improvements.

  * * *

But let's explore an entirely different design. This is a little bit loose; I haven't thought it through totally rigorously.

`SKPaymentTransactionState`, which tells you the status of an in-app purchase transaction, probably would have seemed like a data enum in iOS 3. After all, what states could a transaction take besides `purchasing`, `purchased`, `failed`, or `restored`? But in iOS 8, StoreKit introduced the `deferred` state to handle a new parental-approval feature. Third-party developers did not expect this and had to scramble to handle the unanticipated change.

The frameworks teams often solve this kind of issue by checking the linked SDK version and falling back to compatible behavior in older versions. I don't think StoreKit did this here, but it seems to me that they could have, either by returning the `purchasing` state (which at worst would have stopped users from doing anything else with the app until the purchase was approved or declined) or by returning a `failed` state and then restoring the purchase if it was later approved. At worst, if they had trapped when an incompatible app had a purchase in the `deferred` state, developers might have fixed their bugs more quickly.

I think we could imagine a similar solution being part of our resilience system: Frameworks can add new cases to an enum, but they have to specify compatibility behavior for old `switch` statements. Here's an example design:

A `public enum` may specify the `switch` keyword in its body. (I'm not 100% happy with this keyword, but let's use it for now.) If it does, then the enum is exhaustive:

  // A hypothetical pure-Swift version of `SKPaymentTransaction`.
  @available(iOS 3.0)
  public enum PaymentTransactionState {
    case purchasing
    case purchased(Purchase)
    case restored(Purchase)
    case error(Error)
    
    switch
  }

If it later adds an additional case, or it has non-public cases, it must add a block after the `switch` keyword. The block is called only if `self` is of a case that the calling code doesn't know about; it must either return a value that the caller *does* know about, or trap. So if we added `deferred`, we might instead have:

  @available(iOS 3.0)
  public enum PaymentTransactionState {
    case purchasing
    case purchased(Purchase)
    case restored(Purchase)
    case error(Error)

    @available(iOS 8.0)
    case deferred
    
    switch {
      return .purchasing
    }
  }

(The same logic is applied to the value returned by the block, so if iOS 12 added another case, it could fall back to `deferred`, which would fall back to `purchasing`.)

The `switch` keyword may be followed by a return type; public callers will then need to write their `case` statements as though they were matching against this type. So if, back in iOS 3, you had said this:

  @available(iOS 3.0)
  public enum PaymentTransactionState {
    case purchasing
    case purchased(Purchase)
    case restored(Purchase)
    case error(Error)
    
    switch -> PaymentTransactionState?
  }

Then every `switch` statement on a `PaymentTransactionState` would have had to be written like:

  switch transaction.state {
  case .purchasing?:
    …
  case .purchased?:
    …
  case .restored?:
    …
  case .error?:
    …
  case nil:
    // Handle unexpected states
  }

And then when you added a new case in iOS 8, you could say this, and everyone's code would run through the `nil` path:

  @available(iOS 3.0)
  public enum PaymentTransactionState {
    case purchasing
    case purchased(Purchase)
    case restored(Purchase)
    case error(Error)

    @available(iOS 8.0)
    case deferred
    
    switch -> PaymentTransactionState? {
      return nil
    }
  }

An alternative design would have been to add a `case other` from the start, anticipating that future versions would need to map unknown cases to that one. (Or you could specify `switch -> Never` to forbid switching entirely, or perhaps we could let you say `switch throws` to require the user to say `try switch`. But you get the idea.)

Finally, the kicker: If you do *not* specify an `exhaustive` block, then it is treated as though you had written `switch -> Self? { return nil }`. That is, a "non-exhaustive" enum is just one which turns into an optional when you switch over it, and returns `nil` for unknown cases. Thus, there basically *are* no unknown cases.

Implementation-wise, I imagine that when switching over an enum from `public`, you'd need to make a call which took a version parameter and returned a value compatible with that version. (This might need to be some sort of table of versions, depending on how we end up extending @available to support versions for arbitrary modules.)

  * * *

As for the "untestable code path" problem…maybe we could let you mark certain enum parameters as `@testable`, and then, when brought in through an `@testable import`, allow a `#invalid` value to be passed to those parameters.

  // Library code
  extension PurchasableItem {
    func updateInventory(for state: @testable PaymentTransactionState, quantity: Int) throws {
      switch state {
      case .purchasing:
        return
      case .purchased, .restored:
        inventory += quantity
      case .failed(let error):
        throw error
      default:
        throw ProductError.unknownTransactionState
      }
    }
  }

  // Test
  func testUnknownTransactionState() {
    XCTAssertThrowsError(myProduct.update(for: .#invalid) { error in
      XCTAssertEqual(error, ProductError.unknownTransactionState)
    }
  }

An `@testable` value could not be passed to a non-`@testable` parameter or into a non-`@testable` module, including the actual module the original type came from, unless you had somehow excluded the possibility of an `#invalid` value. You would need to design your code rather carefully to work around this constraint, but I think it could be done.

--
Brent Royal-Gordon
Architechies

_______________________________________________
swift-evolution mailing list
swift-evolution@swift.org
https://lists.swift.org/mailman/listinfo/swift-evolution