Enums and Source Compatibility

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

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

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/\)" 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

(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.

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

- Dave Sweeris

···

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

Hi Jordan,

Thanks for bringing this topic up again! I’m glad to see it will receive attention in Swift 5. I agree with the semantics of your proposed direction.

In terms of syntax, I continue to believe that requiring users to specify a keyword indicating open or closed *in addition* to public would be unfortunate. Open / closed is only relevant for public enums and therefore implies public. We’ve done a really good job of avoiding keyword soup in Swift and the way that open classes are implicitly public is a good precedent that we should follow.

I also continue to believe that aligning protocols, enums and classes to use consistent terminology for similar concepts has many advantages. The semantics would be:

* open: Extensible outside the library
* public: Extensible in future versions of the library (or privately by the library)
* closed: Fixed set of publicly visible cases / subclasses / conformances defined by the library and guaranteed not to change without breaking ABI and source compatibility.

This approach makes public a “soft default” that preserves maximum flexibility for the library author while allowing them to make a stronger guarantee of user-extensibility or completeness by changing (rather than adding) a keyword. It also highlights the symmetry of the two very different user-guarantees a library may choose to support.

As noted in my previous thread, this approach would require a migration for protocols as well as enums as the current behavior of public protocols is to allow conformances outside the library.

There are certainly reasonable arguments to be made for other approaches, particularly if there is no appetite for changing the semantics of public protocols (which seems likely). Nevertheless, I think we should keep the merits of consistency in mind and understand the benefits of alternatives relative to the more consistent approach as we evaluate them.

In terms of alternatives, what is your opinion on using public as a “soft default” and assigning it one of the two enum semantics you discuss? Do you think this makes sense or would you prefer distinct keywords for these two semantics? I don’t have any really great new ideas, but I’ll throw out “complete” and “incomplete” as a possibility.

My two cents for now…

Matthew

···

On Aug 8, 2017, at 5:27 PM, Jordan Rose via swift-evolution <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
https://lists.swift.org/mailman/listinfo/swift-evolution

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.

(1) makes sense
(2) I don’t think this is enforceable. What would prevent a module author from publicly specifying a “closed” enum in version 1, but then changing it and making it open in version 2, thereby breaking everyone who links his module?
(3) is expected
(4) I don’t thinks is possible, because of (2)

So based on this, I think this can be simplified even further:

(A): All enums for which you do not have the source must be “open”, since you can never guarantee that the module/framework might update out-of-band from your app and inadvertently add a new case
(B): All enums for which you do have the source can be “open” or “closed”, as you (somehow) specify, and possibly “closed” by default. This is really just a hint to the compiler for how it builds your app and whether or not it will require a default statement.

Dave

···

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

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

Jordan, I'm glad you're bringing this back up. I think it's clear that
there's appetite for some forward movement in this area.

With respect to syntax--which the conversation in this thread has tackled
first--I agree with the discussion that "open" and "closed" are attractive
but also potentially confusing. As discussed in earlier threads, both
"open" and "closed" will constrain the enum author and/or user in ways
above and beyond "public" currently does, but the terminology does not
necessarily reflect that (as open is the antonym of closed); moreover, the
implications of using these keywords with enums don't necessarily parallel
the implications of using them with classes (for example, an open class can
be subclassed; an open enum that gains additional cases is, if anything,
something of a supertype of the original).

I'd like to suggest a different direction for syntax; I'm putting it
forward because I think the spelling itself naturally suggests a design as
to which enums are (as you call it) "open" or "closed," and how to migrate
existing enums:

enum MyClosedEnum {
  case a
  case b
  case c
}

enum MyOpenEnum {
  case a
  case b
  case c
  default
}

In words, an enum that may have future cases will "leave room" for them by
using the keyword `default`, sort of paralleling its use in a switch
statement. All existing Swift enums can therefore continue to be switched
over exhaustively; that is, this would be an additive, source-compatible
change. For simplicity, we can leave the rules consistent for non-public
and public enums; or, we could prohibit non-public enums from using the
keyword `default` in the manner shown above. Obj-C enums would be imported
as though they declare `default` unless some attribute like
`enum_extensibility` is used to annotate them.

Thoughts?

···

On Tue, Aug 8, 2017 at 5:27 PM, Jordan Rose via swift-evolution < 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

Sorry if I misunderstood the subject, but shouldn't this also be a *consumer* decision, when one wants to keep own switch exhaustive or it is OK to process all possible future cases in 'default'?

If enum is 'closed' - nothing is changed, as I understand, we have the same rules for switch as currently.

If 'external' enum, from other framework, was marked as 'closed' but was actually changed in new version of framework/module - our source code will fail to compile (because we need fix our switch cases) and our binary module(built with previous version of framework) will crash.

With 'open' enums, depending on situations, I as a consumer of external framework, can decide that it is important to me, in my code, check *each* value of external enum in switch. If new enum case added/changed in external framework - my code must fail to compile and notify me that new case should be processed.
Once we added 'default' case in 'switch' in our code for 'open' enum - we lost compiler's support to keep our 'switch' exhaustive.

But from other side, in other situation, I want to process all new cases for 'open' enum in 'default' branch, and so allow my source/compiled code to work with new version of framework(with added cases).

So, it seems like in both situations we need to explicitly tell what is our decided behavior for new cases for 'open' enum in exhaustive switch : crash/fail to compile or process in 'default' block.

What if in case of exhaustive switch on 'open' enum, compiler warns us saying "this is an exhaustive switch on 'open' enum, but cases can be added later. clarify what is your decided behaviour. by default your code will crash if new cases are added"
, then we can add 'default' block to process new future cases or somehow mark that switch as explicitly exhaustive... something like this:

@exhaustive
switch openEnum {
   case .yes : ...
   case .no : ...
}

What I'm missing?

···

On 09.08.2017 1:27, Jordan Rose via swift-evolution 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

A question: Does it make sense to switch on a non-exhuastive enum at all? Maybe, from the public vantage point, a closed enum should be indistinguishable from a struct with static properties/methods—if you can switch over one, it's only through its conformance to `Equatable` or its implementation of a public `~=` operator.

(IIRC, we ended up somewhere quite similar to this with our "import Objective-C string constants as enums" feature—it actually ends up creating structs instead.)

···

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

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.

--
Brent Royal-Gordon
Architechies

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

···

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

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

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

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

There really isn't a good way to do "extensible outside the library" enums, because at that point you've lost the guarantee of distinct cases—it's possible for two modules to add the same case with the same raw value. So I don't think there are really three states for public enums, only two. I do sympathize with the keyword soup problem, though.

(Also, let's please leave protocols out of the enum discussion. Nearly all the reasons to give protocols a public/open distinction come from classes, not enums, and I wouldn't want to tie the success or failure of one proposal to the other.)

The issue of a default is an important one; I'll respond to it on Adrian's branch of the thread.

Jordan

···

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

Hi Jordan,

Thanks for bringing this topic up again! I’m glad to see it will receive attention in Swift 5. I agree with the semantics of your proposed direction.

In terms of syntax, I continue to believe that requiring users to specify a keyword indicating open or closed *in addition* to public would be unfortunate. Open / closed is only relevant for public enums and therefore implies public. We’ve done a really good job of avoiding keyword soup in Swift and the way that open classes are implicitly public is a good precedent that we should follow.

I also continue to believe that aligning protocols, enums and classes to use consistent terminology for similar concepts has many advantages. The semantics would be:

* open: Extensible outside the library
* public: Extensible in future versions of the library (or privately by the library)
* closed: Fixed set of publicly visible cases / subclasses / conformances defined by the library and guaranteed not to change without breaking ABI and source compatibility.

This approach makes public a “soft default” that preserves maximum flexibility for the library author while allowing them to make a stronger guarantee of user-extensibility or completeness by changing (rather than adding) a keyword. It also highlights the symmetry of the two very different user-guarantees a library may choose to support.

As noted in my previous thread, this approach would require a migration for protocols as well as enums as the current behavior of public protocols is to allow conformances outside the library.

There are certainly reasonable arguments to be made for other approaches, particularly if there is no appetite for changing the semantics of public protocols (which seems likely). Nevertheless, I think we should keep the merits of consistency in mind and understand the benefits of alternatives relative to the more consistent approach as we evaluate them.

In terms of alternatives, what is your opinion on using public as a “soft default” and assigning it one of the two enum semantics you discuss? Do you think this makes sense or would you prefer distinct keywords for these two semantics? I don’t have any really great new ideas, but I’ll throw out “complete” and “incomplete” as a possibility.

My two cents for now…

Matthew

On Aug 8, 2017, at 5:27 PM, 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

This is true, but also applies to things like adding fields to structs, or adding parameters with default values to functions. If a library author isn't careful, they can break source or binary compatibility; that's not different for enums, and in fact I'm hoping that an explicit "closed" annotation makes it rarer. (Additionally, we do want to have tools that can help check this kind of thing.)

To put it another way, it really wouldn't be acceptable for a Swift-defined NSComparisonResult to be "open", even though Foundation is a binary framework.

Jordan

···

On Aug 9, 2017, at 11:45, Dave DeLong <delong@apple.com> wrote:

On Aug 8, 2017, at 4:27 PM, 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.

(1) makes sense
(2) I don’t think this is enforceable. What would prevent a module author from publicly specifying a “closed” enum in version 1, but then changing it and making it open in version 2, thereby breaking everyone who links his module?
(3) is expected
(4) I don’t thinks is possible, because of (2)

So based on this, I think this can be simplified even further:

(A): All enums for which you do not have the source must be “open”, since you can never guarantee that the module/framework might update out-of-band from your app and inadvertently add a new case
(B): All enums for which you do have the source can be “open” or “closed”, as you (somehow) specify, and possibly “closed” by default. This is really just a hint to the compiler for how it builds your app and whether or not it will require a default statement.

(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?)

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.

···

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:

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

Hi, Vladimir. I think framing this as a consumer decision is the wrong place to start. There are some enums that definitely make sense to be "closed", all the time, with no additional annotations, including Foundation.NSComparisonResult and, well, Swift.Optional. (Yes, Optional is special, and we could always handle it specially if we needed to, but it would be nice if it used the same logic as everything else.)

Beyond that, I think something like what you and Charlie describe (your '@exhaustive', Charlie's 'switch!') could make sense, as a way to provide a deterministic behavior while still getting compiler warnings for not being exhaustive. (Probably the only supported behavior here would be trapping, since there is no way to test such a scenario with the libraries you currently have.) But I'd like to see a real-world example of exhaustively switching over an enum in the SDK before designing a feature around this; I strongly suspect it's not something we need…

…in the binary framework case. It may still be interesting for source frameworks, particularly C enums in those source frameworks.

Jordan

···

On Aug 9, 2017, at 09:57, Vladimir.S <svabox@gmail.com> wrote:

Sorry if I misunderstood the subject, but shouldn't this also be a *consumer* decision, when one wants to keep own switch exhaustive or it is OK to process all possible future cases in 'default'?

If enum is 'closed' - nothing is changed, as I understand, we have the same rules for switch as currently.

If 'external' enum, from other framework, was marked as 'closed' but was actually changed in new version of framework/module - our source code will fail to compile (because we need fix our switch cases) and our binary module(built with previous version of framework) will crash.

With 'open' enums, depending on situations, I as a consumer of external framework, can decide that it is important to me, in my code, check *each* value of external enum in switch. If new enum case added/changed in external framework - my code must fail to compile and notify me that new case should be processed.
Once we added 'default' case in 'switch' in our code for 'open' enum - we lost compiler's support to keep our 'switch' exhaustive.

But from other side, in other situation, I want to process all new cases for 'open' enum in 'default' branch, and so allow my source/compiled code to work with new version of framework(with added cases).

So, it seems like in both situations we need to explicitly tell what is our decided behavior for new cases for 'open' enum in exhaustive switch : crash/fail to compile or process in 'default' block.

What if in case of exhaustive switch on 'open' enum, compiler warns us saying "this is an exhaustive switch on 'open' enum, but cases can be added later. clarify what is your decided behaviour. by default your code will crash if new cases are added"
, then we can add 'default' block to process new future cases or somehow mark that switch as explicitly exhaustive... something like this:

@exhaustive
switch openEnum {
case .yes : ...
case .no : ...
}

What I'm missing?

On 09.08.2017 1:27, Jordan Rose via swift-evolution 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

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.

···

Am 9. August 2017 um 18:08:09, Matthew Johnson via swift-evolution (swift-evolution@swift.org) schrieb:

Hi Jordan,

Thanks for bringing this topic up again! I’m glad to see it will receive attention in Swift 5. I agree with the semantics of your proposed direction.

In terms of syntax, I continue to believe that requiring users to specify a keyword indicating open or closed *in addition* to public would be unfortunate. Open / closed is only relevant for public enums and therefore implies public. We’ve done a really good job of avoiding keyword soup in Swift and the way that open classes are implicitly public is a good precedent that we should follow.

I also continue to believe that aligning protocols, enums and classes to use consistent terminology for similar concepts has many advantages. The semantics would be:

* open: Extensible outside the library
* public: Extensible in future versions of the library (or privately by the library)
* closed: Fixed set of publicly visible cases / subclasses / conformances defined by the library and guaranteed not to change without breaking ABI and source compatibility.

This approach makes public a “soft default” that preserves maximum flexibility for the library author while allowing them to make a stronger guarantee of user-extensibility or completeness by changing (rather than adding) a keyword. It also highlights the symmetry of the two very different user-guarantees a library may choose to support.

As noted in my previous thread, this approach would require a migration for protocols as well as enums as the current behavior of public protocols is to allow conformances outside the library.

There are certainly reasonable arguments to be made for other approaches, particularly if there is no appetite for changing the semantics of public protocols (which seems likely). Nevertheless, I think we should keep the merits of consistency in mind and understand the benefits of alternatives relative to the more consistent approach as we evaluate them.

In terms of alternatives, what is your opinion on using public as a “soft default” and assigning it one of the two enum semantics you discuss? Do you think this makes sense or would you prefer distinct keywords for these two semantics? I don’t have any really great new ideas, but I’ll throw out “complete” and “incomplete” as a possibility.

My two cents for now…

Matthew

On Aug 8, 2017, at 5:27 PM, Jordan Rose via swift-evolution <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" 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

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

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.

Hi, Vladimir. I think framing this as a consumer decision is the wrong place to start. There are some enums that definitely make sense to be "closed", all the time, with no additional annotations, including Foundation.NSComparisonResult and, well, Swift.Optional. (Yes, Optional is special, and we could always handle it specially if we needed to, but it would be /nice/ if it used the same logic as everything else.)

Beyond that, I think something like what you and Charlie describe (your '@exhaustive', Charlie's 'switch!') could make sense, as a way to provide a deterministic behavior while still getting compiler warnings for not being exhaustive. (Probably the only supported behavior here would be trapping, since there is no way to test such a scenario with the libraries you currently have.) But I'd like to see a real-world example of exhaustively switching over an enum in the SDK before designing a feature around this; I strongly suspect it's not something we need…

…/in the binary framework case/. It may still be interesting for source frameworks, particularly C enums in those source frameworks.

Hi Jordan. Thank you for comments. It seems like I'm missing something important.. Will only SDK framework contains open enums? Can't I wish to have an exhaustive switch in my own code regarding some open enum coming from some other's framework and to be sure I'm processing all the cases during the compilation time of my code with that framework?

I assume everybody(ok,most of us) thanks Swift for helping us keep switch exhaustive when we *need* this, so we will not forget to process new *known* cases, and I don't understand why this can be changed with 'open' enums.

Yes, for open enums we *have to* process 'future' cases in switch(even if it is exhaustive at the moment of compilation), but also for some open enums we *will* need a help from Swift compiler to keep our switch exhaustive for the moment of compilation.

And for such switch(exhaustive for known at compilation time cases of open enum), we probably should introduce some 'future' keyword:

switch openEnum {
   case .yes: ..
   case .no: ..
   future: ... // this is for future cases only, all known cases should be processed
}

And you can use 'default' as usual, if you don't care of *any* other cases(including future cases) in this open enum:

switch openEnum {
   case .yes: ..
   case .no: ..
   default: ...
}

The 'future' or 'default' would be then required for switch on open enum instance.

Vladimir.

···

On 10.08.2017 3:58, Jordan Rose wrote:

Jordan

On Aug 9, 2017, at 09:57, Vladimir.S <svabox@gmail.com <mailto:svabox@gmail.com>> >> wrote:

Sorry if I misunderstood the subject, but shouldn't this also be a *consumer* decision, when one wants to keep own switch exhaustive or it is OK to process all possible future cases in 'default'?

If enum is 'closed' - nothing is changed, as I understand, we have the same rules for switch as currently.

If 'external' enum, from other framework, was marked as 'closed' but was actually changed in new version of framework/module - our source code will fail to compile (because we need fix our switch cases) and our binary module(built with previous version of framework) will crash.

With 'open' enums, depending on situations, I as a consumer of external framework, can decide that it is important to me, in my code, check *each* value of external enum in switch. If new enum case added/changed in external framework - my code must fail to compile and notify me that new case should be processed.
Once we added 'default' case in 'switch' in our code for 'open' enum - we lost compiler's support to keep our 'switch' exhaustive.

But from other side, in other situation, I want to process all new cases for 'open' enum in 'default' branch, and so allow my source/compiled code to work with new version of framework(with added cases).

So, it seems like in both situations we need to explicitly tell what is our decided behavior for new cases for 'open' enum in exhaustive switch : crash/fail to compile or process in 'default' block.

What if in case of exhaustive switch on 'open' enum, compiler warns us saying "this is an exhaustive switch on 'open' enum, but cases can be added later. clarify what is your decided behaviour. by default your code will crash if new cases are added"
, then we can add 'default' block to process new future cases or somehow mark that switch as explicitly exhaustive... something like this:

@exhaustive
switch openEnum {
case .yes : ...
case .no : ...
}

What I'm missing?

On 09.08.2017 1:27, Jordan Rose via swift-evolution 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

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.

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