[swift-evolution] Enums and Source Compatibility

Vladimir.S svabox at gmail.com
Thu Aug 10 11:14:51 CDT 2017


On 10.08.2017 18:22, Adrian Zubarev via swift-evolution wrote:
> I think this design does not avoid you writing something like `private enum Foo { 
> default ... }`, which is redudant as Jordan already pointed out in his previous post, 
> nor does it have a consistent way of declaration:

Why compiler can't require 'open' enums to be public only? So, you can write but this 
will not compile. No?

> 
> enum Foo {
>    case abc
>    case def
>    default
> }
> 
> enum Foo {
>    case abc
>    default
>    case def
> }
> 
> enum Foo {
>    default
>    case abc
>    case def
> }
> 
> ----

Why compiler can't require 'default' be declared only after last 'case' ?

> 
> On the other hand I'd be very much in favor of the design David has pitched, which 
> makes `public` as a soft default for public API's and adds a stronger constraints 
> with an extra keyword OR a different access modifier. In case of an access modifier 
> it would be really interesting if someone knows other use cases where we could reuse 
> `closed` for similar purposes.
> 
> Long story short:
> - public is the soft default and all uses of an enum from module A in module B would 
> require `default` in swithch statements
> - closed is the stronger implied `public` which makes the enum finite and does not 
> require `default` if you switch on all cases

Can't agree. Default-like 'public' for class also allows compiler to make its 
optimizations as it is known that there can't be a subclass of published class.

'public' for enum will in inverse block optimizations, as there is no guarantee that 
enum will be the same and will not change in future.

IMO the developer of external API should explicitly mark public enum as 'closed' or 
as 'open', to be concrete if changes in enum should/will lead to new major version of 
framework(so can't be changed in current version) or most likely this enum will be 
extended in next minor update and then "it might suddenly grow a new case containing 
a struct with 5000 Strings in it"(Jordan Rose)

Vladimir.

> -- 
> Adrian Zubarev
> Sent with Airmail
> 
> Am 10. August 2017 um 16:55:00, Matthew Johnson via swift-evolution 
> (swift-evolution at swift.org <mailto:swift-evolution at swift.org>) schrieb:
> 
>>
>>> On Aug 10, 2017, at 9:25 AM, Vladimir.S <svabox at gmail.com 
>>> <mailto:svabox at gmail.com>> wrote:
>>>
>>> On 10.08.2017 16:46, Matthew Johnson via swift-evolution wrote:
>>>>> On Aug 10, 2017, at 7:46 AM, James Froggatt via swift-evolution
>>>>> <swift-evolution at swift.org <mailto:swift-evolution at swift.org>> wrote:
>>>>> Since it seems to have been lost in the noise, I want to second with support for
>>>>> Xiaodi's syntax of having `default` appearing in the enum declaration itself.
>>>>> It's much clearer in its intention, feels very ‘Swifty’, and more importantly it
>>>>> doesn't prompt whole threads debating the semantics of `open` vs `public`.
>>>> I think Xiaodi’s syntax is very elegant if we want to avoid the access control
>>>> style syntax.  However, it does one problem: the “error of omission” (not thinking
>>>> about open vs closed) leaves a library author with a closed enum, preventing them
>>>> from adding cases in the future without breaking compatibility.  I’m not sure this
>>>> is acceptable.
>>>
>>> Then, doesn't this mean that any 'usual' enum should be 'open' by default, and 
>>> only enum declared with some marker (like 'final' or 'enum(sealed)') can be 'closed'?
>>>
>>> Otherwise we need to require an explicit marker for *each* enum, and so break the 
>>> source compatibility? (we'll have to append that marker to each enum in your 
>>> current code)
>>
>> This is a good point.  A good first decision is whether we prioritize source 
>> compatibility or the more conservative “default”.  If source compatibility is 
>> prioritized Xiaodi’s proposed syntax is probably hard to beat.
>>
>>>
>>> Also I'd suggest this for closed enum:
>>>
>>> enum MyClosedEnum {
>>>  case a
>>>  case b
>>>  case c
>>>  final
>>> }
>>>
>>> So, for public closed enum it will looks like:
>>>
>>> public enum MyClosedEnum {
>>>  case a
>>>  case b
>>>  case c
>>>  final
>>> }
>>>
>>> Also, if we need to explicitly mark open enum, probably we can consider 'continue' 
>>> keyword, as IMO is not clear what 'default' is saying on declaration site('you 
>>> must insert `default` in switch'? 'there are other `default` cases'?) :
>>>
>>> public enum MyOpenEnum {
>>>  case a
>>>  case b
>>>  case c
>>>  continue // to be continue...
>>> }
>>>
>>>
>>>>> ------------ Begin Message ------------ Group: gmane.comp.lang.swift.evolution 
>>>>> MsgID: <CAGY80u=kVQA1q=5TMxXxFgM4tLGFUQh61EN1daepEMAA_FoE9Q at mail.gmail.com 
>>>>> <mailto:CAGY80u=kVQA1q=5TMxXxFgM4tLGFUQh61EN1daepEMAA_FoE9Q at mail.gmail.com>>
>>>>> On Tue, Aug 8, 2017 at 5:27 PM, Jordan Rose via swift-evolution < 
>>>>> swift-evolution-m3FHrko0VLzYtjvyW6yDsg at public.gmane.org 
>>>>> <mailto:swift-evolution-m3FHrko0VLzYtjvyW6yDsg at public.gmane.org>> wrote:
>>>>>> Hi, everyone. Now that Swift 5 is starting up, I'd like to circle back to an
>>>>>> issue that's been around for a while: the source compatibility of enums. Today, 
>>>>>> it's an error to switch over an enum without handling all the cases, but this 
>>>>>> breaks down in a number of ways:
>>>>>> - A C enum may have "private cases" that aren't defined inside the original
>>>>>> enum declaration, and there's no way to detect these in a switch without
>>>>>> dropping down to the rawValue. - For the same reason, the compiler-synthesized
>>>>>> 'init(rawValue:)' on an imported enum never produces 'nil', because who knows
>>>>>> how anyone's using C enums anyway? - Adding a new case to a *Swift* enum in a
>>>>>> library breaks any client code that was trying to switch over it.
>>>>>> (This list might sound familiar, and that's because it's from a message of mine 
>>>>>> on a thread started by Matthew Johnson back in February called "[Pitch]
>>>>>> consistent public access modifiers". Most of the rest of this email is going
>>>>>> to go the same way, because we still need to make progress here.)
>>>>>> At the same time, we really like our exhaustive switches, especially over enums 
>>>>>> we define ourselves. And there's a performance side to this whole thing
>>>>>> too; if all cases of an enum are known, it can be passed around much more
>>>>>> efficiently than if it might suddenly grow a new case containing a struct with
>>>>>> 5000 Strings in it.
>>>>>> *Behavior*
>>>>>> I think there's certain behavior that is probably not *terribly* controversial:
>>>>>> - When enums are imported from Apple frameworks, they should always require a
>>>>>> default case, except for a few exceptions like NSRectEdge. (It's Apple's job
>>>>>> to handle this and get it right, but if we get it wrong with an imported enum
>>>>>> there's still the workaround of dropping down to the raw value.) - When I
>>>>>> define Swift enums in the current framework, there's obviously no compatibility 
>>>>>> issues; we should allow exhaustive switches.
>>>>>> Everything else falls somewhere in the middle, both for enums defined in 
>>>>>> Objective-C:
>>>>>> - If I define an Objective-C enum in the current framework, should it allow
>>>>>> exhaustive switching, because there are no compatibility issues, or not,
>>>>>> because there could still be private cases defined in a .m file? - If there's
>>>>>> an Objective-C enum in *another* framework (that I built locally with Xcode,
>>>>>> Carthage, CocoaPods, SwiftPM, etc.), should it allow exhaustive switching,
>>>>>> because there are no *binary* compatibility issues, or not, because there may
>>>>>> be *source* compatibility issues? We'd really like adding a new enum case to
>>>>>> *not* be a breaking change even at the source level. - If there's an
>>>>>> Objective-C enum coming in through a bridging header, should it allow
>>>>>> exhaustive switching, because I might have defined it myself, or not, because
>>>>>> it might be non-modular content I've used the bridging header to import?
>>>>>> And in Swift:
>>>>>> - If there's a Swift enum in another framework I built locally, should it allow 
>>>>>> exhaustive switching, because there are no binary compatibility issues,
>>>>>> or not, because there may be source compatibility issues? Again, we'd really
>>>>>> like adding a new enum case to *not* be a breaking change even at the source
>>>>>> level.
>>>>>> Let's now flip this to the other side of the equation. I've been talking about
>>>>>> us disallowing exhaustive switching, i.e. "if the enum might grow new cases
>>>>>> you must have a 'default' in a switch". In previous (in-person) discussions
>>>>>> about this feature, it's been pointed out that the code in an 
>>>>>> otherwise-fully-covered switch is, by definition, unreachable, and therefore
>>>>>> untestable. This also isn't a desirable situation to be in, but it's mitigated
>>>>>> somewhat by the fact that there probably aren't many framework enums you
>>>>>> should exhaustively switch over anyway. (Think about Apple's frameworks
>>>>>> again.) I don't have a great answer, though.
>>>>>> For people who like exhaustive switches, we thought about adding a new kind of
>>>>>> 'default'—let's call it 'unknownCase' just to be able to talk about it. This
>>>>>> lets you get warnings when you update to a new SDK, but is even more likely to
>>>>>> be untested code. We didn't think this was worth the complexity.
>>>>>> *Terminology*
>>>>>> The "Library Evolution <http://jrose-apple.github.io/swift-library-evolution/>" 
>>>>>> doc (mostly written
>>>>>> by me) originally called these "open" and "closed" enums ("requires a default"
>>>>>> and "allows exhaustive switching", respectively), but this predated the use of
>>>>>> 'open' to describe classes and class members. Matthew's original thread did
>>>>>> suggest using 'open' for enums as well, but I argued against that, for a few
>>>>>> reasons:
>>>>>> - For classes, "open" and "non-open" restrict what the *client* can do. For
>>>>>> enums, it's more about providing the client with additional guarantees—and
>>>>>> "non-open" is the one with more guarantees. - The "safe" default is backwards:
>>>>>> a merely-public class can be made 'open', while an 'open' class cannot be made
>>>>>> non-open. Conversely, an "open" enum can be made "closed" (making default
>>>>>> cases unnecessary), but a "closed" enum cannot be made "open".
>>>>>> That said, Clang now has an 'enum_extensibility' attribute that does take 
>>>>>> 'open' or 'closed' as an argument.
>>>>>> On Matthew's thread, a few other possible names came up, though mostly only
>>>>>> for the "closed" case:
>>>>>> - 'final': has the right meaning abstractly, but again it behaves differently
> 
> 
> _______________________________________________
> swift-evolution mailing list
> swift-evolution at swift.org
> https://lists.swift.org/mailman/listinfo/swift-evolution
> 


More information about the swift-evolution mailing list