<html><head><meta http-equiv="Content-Type" content="text/html; charset=utf-8"></head><body style="word-wrap: break-word; -webkit-nbsp-mode: space; line-break: after-white-space;" class="">Hi Brent,<div class=""><br class=""></div><div class="">Thanks for the analysis of the issues here. I think you’re right that there are two major types of enums, and that there are a set developers tend to switch on that are usually fixed, where additional cases won’t make sense.</div><div class=""><br class=""></div><div class="">I think there are several issues that I see with these different ideas, and they stem back to a similar source:</div><div class=""><br class=""></div><div class="">1. They are overly complex solutions to a specific problem: inter-library variable enums. We would be destroying some of the awesome simplicity and design characteristics of enums in Swift to deal with an outlying problem.</div><div class=""><br class=""></div><div class="">2. Project enums should always be exhaustive. Your design introduces ‘exhaustive’ and has an end goal of removing ‘@nonexhaustive’ but the fact is that keyword should be the default internal of a project. It seems an odd trajectory change to make purely for public enums.</div><div class=""><br class=""></div><div class="">It would seem much simpler to have a word that denotes that an enum is not considered ‘exhaustive’ in public code, or to force users to annotate their intention of exhaustive vs extensible when they make their enum public, and have intra-project enums remain exhaustive. This then requires a simple addition of a default argument to handle outlier cases you may encounter. Your solutions seem to me to require a large to deal with a relatively narrow use case, radically changing the enum system.</div><div class=""><br class=""></div><div class="">I agree that we could include something as part of @testable to add an additional case to test the edge case. We do have specific language elements for testing, why not here?</div><div class=""><br class=""></div><div class="">- Rod</div><div class=""><br class=""></div><div class=""><div><br class=""><blockquote type="cite" class=""><div class="">On 6 Sep 2017, at 10:53 pm, Brent Royal-Gordon via swift-evolution &lt;<a href="mailto:swift-evolution@swift.org" class="">swift-evolution@swift.org</a>&gt; wrote:</div><br class="Apple-interchange-newline"><div class=""><meta http-equiv="Content-Type" content="text/html charset=utf-8" class=""><div style="word-wrap: break-word; -webkit-nbsp-mode: space; -webkit-line-break: after-white-space;" class=""><div class=""><blockquote type="cite" class=""><div class="">On Sep 5, 2017, at 5:19 PM, Jordan Rose via swift-evolution &lt;<a href="mailto:swift-evolution@swift.org" class="">swift-evolution@swift.org</a>&gt; wrote:</div><br class="Apple-interchange-newline"><div class=""><meta http-equiv="Content-Type" content="text/html; charset=utf-8" class=""><div style="word-wrap: break-word; -webkit-nbsp-mode: space; line-break: after-white-space;" class=""><div class="">I've taken everyone's feedback into consideration and written this up as a proposal:&nbsp;<a href="https://github.com/jrose-apple/swift-evolution/blob/non-exhaustive-enums/proposals/nnnn-non-exhaustive-enums.md" class="">https://github.com/jrose-apple/swift-evolution/blob/non-exhaustive-enums/proposals/nnnn-non-exhaustive-enums.md</a>. The next step is working on an implementation, but if people have further pre-review comments I'd be happy to hear them.</div></div></div></blockquote><br class=""></div><div class="">I disagree with the choice of `exhaustive` and `nonexhaustive`. They are too long; the more resilient keyword is longer than the more fragile one (and difficult to read!); and they don't match the clang annotation. We may have to compromise on one or two of these, but the combination of all three ought to be considered disqualifying.</div><div class=""><br class=""></div><div class="">I think `final`/`nonfinal`, `total`/`partial`, `fixed`/? or `permanent`/? are all better because they're shorter, although they all have problems with their antonyms. `candid`/`coy` or `candid`/`shy` produce the right soft default, but are kind of weirdly figurative.</div><div class=""><br class=""></div><div class="">But I don't think a change of keywords will fix everything here. Fundamentally, I am not convinced that source compatibility of `switch` statements should be weighed so heavily. Based on your survey of Foundation, you suggest that the vast majority of imported enums should source-break all switches in Swift 5. Why is that acceptable, but making Swift enums source-breaking unacceptable?</div><div class=""><br class=""></div><div class="">I suspect that, in practice, `public` enums tend to fall into two categories:</div><div class=""><br class=""></div><div class=""><div class=""><span class="Apple-tab-span" style="white-space: pre;">        </span>1. "Data enums" which represent important data that happens to consist of a set of alternatives. Outside users will frequently need to switch over these, but they are not very likely to evolve or have private cases.</div></div><div class=""><br class=""></div><div class=""><span class="Apple-tab-span" style="white-space:pre">        </span>2. "Mode enums" which tweak the behavior of an API. These are very likely to evolve or have private cases, but outside users are not very likely to need to switch over them.</div><div class=""><br class=""></div><div class=""><div class="">An example of a data enum would be, as you mentioned, `NSComparisonResult`. People really *do* need to be able to test against it, but barring some fundamental break in the nature of reality, it will only ever have those three cases. So it's fine to make it exhaustive.</div><div class=""><br class=""></div></div><div class="">An example of a mode enum would be `UIViewAnimationCurve`, which tells UIKit how to ease an animation. I chose that example because I actually traced a bug just last week to my mistaken impression that this enum had no private cases. I was mapping values of this type to their corresponding `UIViewAnimationOptions` values; because there were private cases, this was Objective-C code, and I didn't include sufficiently aggressive assertions, I ended up reading garbage data from memory. But while debugging this, it struck me that this was actually *really weird* code. How often do you, as a developer outside UIKit, need to interpret the value of a type like `UIViewAnimationCurve`? If the compiler suddenly changed the exhaustiveness behavior of `UIViewAnimationCurve`, probably less than 1% of apps would even notice—and the affected code would probably have latent bugs!</div><div class=""><br class=""></div><div class="">Here's my point: Suddenly treating a mode enum as non-exhaustive is *technically* source-breaking, but *people aren't doing things to them that would break*. It is only the data enums that would actually experience source breakage, and we both seem to agree those are relatively uncommon. So I would argue the relatively rare source breaks are acceptable.</div><div class=""><br class=""></div><div class="">Basically, what I would suggest is this:</div><div class=""><br class=""></div><div class=""><span class="Apple-tab-span" style="white-space:pre">        </span>1. In Swift 4.1, we should add a permanent `exhaustive`* keyword and a temporary `@nonexhaustive` attribute to Swift. These are no-ops, or maybe `@nonexhaustive` simply silences the "unreachable default case" warning.</div><div class=""><br class=""></div><div class=""><span class="Apple-tab-span" style="white-space:pre">        </span>2. In Swift 4.2 (or whatever Swift 5's Swift 4 mode is called), we should warn about any enum which does not have either `exhaustive` or `@nonexhaustive` attached to it, but publishes them as non-exhaustive. `switch` requires a `default` case for any non-exhaustive public enum.</div><div class=""><br class=""></div><div class=""><span class="Apple-tab-span" style="white-space:pre">        </span>3. Swift 5 in Swift 5 mode does the same thing, but does *not* warn about the absence of `@nonexhaustive`.</div><div class=""><br class=""></div><div class=""><span class="Apple-tab-span" style="white-space:pre">        </span>4. Swift 5 importing Objective-C treats enums as non-exhaustive by default, unless marked with an attribute.</div><div class=""><br class=""></div><div class="">The dummy keywords in Swift 4.1 ensure that developers can write code that works in both a true Swift 4 compiler and a Swift 5 compiler in Swift 4 mode. (If we don't like that approach, though, we can bump the versions—give Swift 4.2 the behavior I described for Swift 4, give Swift 5 the behavior I described for 4.2, and plan to give Swift 6 the behavior I described for Swift 5.)</div><div class=""><br class=""></div><div class="">* I'm still not super-happy with `exhaustive`, but since `@nonexhaustive` is temporary in this scheme, that at least improves one of the complaints about it. I think the keywords I discussed above would still be improvements.</div><div class=""><br class=""></div><div class=""><span class="Apple-tab-span" style="white-space:pre">        </span>* * *</div><div class=""><br class=""></div><div class="">But let's explore an entirely different design. This is a little bit loose; I haven't thought it through totally rigorously.</div><div class=""><br class=""></div><div class="">`SKPaymentTransactionState`, which tells you the status of an in-app purchase transaction, probably would have seemed like a data enum in iOS 3. After all, what states could a transaction take besides `purchasing`, `purchased`, `failed`, or `restored`? But in iOS 8, StoreKit introduced the `deferred` state to handle a new parental-approval feature. Third-party developers did not expect this and had to scramble to handle the unanticipated change.</div><div class=""><br class=""></div><div class="">The frameworks teams often solve this kind of issue by checking the linked SDK version and falling back to compatible behavior in older versions. I don't think StoreKit did this here, but it seems to me that they could have, either by returning the `purchasing` state (which at worst would have stopped users from doing anything else with the app until the purchase was approved or declined) or by returning a `failed` state and then restoring the purchase if it was later approved. At worst, if they had trapped when an incompatible app had a purchase in the `deferred` state, developers might have fixed their bugs more quickly.</div><div class=""><br class=""></div><div class="">I think we could imagine a similar solution being part of our resilience system: Frameworks can add new cases to an enum, but they have to specify compatibility behavior for old `switch` statements. Here's an example design:</div><div class=""><br class=""></div><div class="">A `public enum` may specify the `switch` keyword in its body. (I'm not 100% happy with this keyword, but let's use it for now.) If it does, then the enum is exhaustive:</div><div class=""><br class=""></div><div class=""><span class="Apple-tab-span" style="white-space:pre">        </span>// A hypothetical pure-Swift version of `SKPaymentTransaction`.</div><div class=""><span class="Apple-tab-span" style="white-space: pre;">        </span>@available(iOS 3.0)</div><div class=""><span class="Apple-tab-span" style="white-space: pre;">        </span>public enum PaymentTransactionState {</div><div class=""><span class="Apple-tab-span" style="white-space: pre;">                </span>case purchasing</div><div class=""><span class="Apple-tab-span" style="white-space: pre;">                </span>case purchased(Purchase)</div><div class=""><span class="Apple-tab-span" style="white-space: pre;">                </span>case restored(Purchase)</div><div class=""><span class="Apple-tab-span" style="white-space: pre;">                </span>case error(Error)</div><div class=""><span class="Apple-tab-span" style="white-space: pre;">                </span></div><div class=""><span class="Apple-tab-span" style="white-space: pre;">                </span>switch</div><div class=""><span class="Apple-tab-span" style="white-space: pre;">        </span>}</div><div class=""><br class=""></div><div class="">If it later adds an additional case, or it has non-public cases, it must add a block after the `switch` keyword. The block is called only if `self` is of a case that the calling code doesn't know about; it must either return a value that the caller *does* know about, or trap. So if we added `deferred`, we might instead have:</div><div class=""><br class=""></div><div class=""><div class=""><span class="Apple-tab-span" style="white-space: pre;">        </span>@available(iOS 3.0)</div><div class=""><span class="Apple-tab-span" style="white-space: pre;">        </span>public enum&nbsp;PaymentTransactionState&nbsp;{</div><div class=""><span class="Apple-tab-span" style="white-space: pre;">                </span>case purchasing</div><div class=""><span class="Apple-tab-span" style="white-space: pre;">                </span>case purchased(Purchase)</div><div class=""><span class="Apple-tab-span" style="white-space: pre;">                </span>case restored(Purchase)</div><div class=""><span class="Apple-tab-span" style="white-space: pre;">                </span>case error(Error)</div><div class=""><br class=""></div><div class=""><span class="Apple-tab-span" style="white-space:pre">                </span>@available(iOS 8.0)</div><div class=""><span class="Apple-tab-span" style="white-space:pre">                </span>case deferred</div><div class=""><span class="Apple-tab-span" style="white-space: pre;">                </span></div><div class=""><span class="Apple-tab-span" style="white-space: pre;">                </span>switch {</div><div class=""><span class="Apple-tab-span" style="white-space:pre">                        </span>return .purchasing</div><div class=""><span class="Apple-tab-span" style="white-space:pre">                </span>}</div><div class=""><span class="Apple-tab-span" style="white-space: pre;">        </span>}</div></div><div class=""><br class=""></div><div class="">(The same logic is applied to the value returned by the block, so if iOS 12 added another case, it could fall back to `deferred`, which would fall back to `purchasing`.)</div><div class=""><br class=""></div><div class="">The `switch` keyword may be followed by a return type; public callers will then need to write their `case` statements as though they were matching against this type. So if, back in iOS 3, you had said this:</div><div class=""><br class=""></div><div class=""><div class=""><span class="Apple-tab-span" style="white-space: pre;">        </span>@available(iOS 3.0)</div><div class=""><span class="Apple-tab-span" style="white-space: pre;">        </span>public enum PaymentTransactionState {</div><div class=""><span class="Apple-tab-span" style="white-space: pre;">                </span>case purchasing</div><div class=""><span class="Apple-tab-span" style="white-space: pre;">                </span>case purchased(Purchase)</div><div class=""><span class="Apple-tab-span" style="white-space: pre;">                </span>case restored(Purchase)</div><div class=""><span class="Apple-tab-span" style="white-space: pre;">                </span>case error(Error)</div><div class=""><span class="Apple-tab-span" style="white-space: pre;">                </span></div><div class=""><span class="Apple-tab-span" style="white-space: pre;">                </span>switch -&gt; PaymentTransactionState?</div><div class=""><span class="Apple-tab-span" style="white-space: pre;">        </span>}</div></div><div class=""><br class=""></div><div class="">Then every `switch` statement on a `PaymentTransactionState` would have had to be written like:</div><div class=""><br class=""></div><div class=""><span class="Apple-tab-span" style="white-space:pre">        </span>switch transaction.state {</div><div class=""><span class="Apple-tab-span" style="white-space:pre">        </span>case .purchasing?:</div><div class=""><span class="Apple-tab-span" style="white-space:pre">                </span>…</div><div class=""><span class="Apple-tab-span" style="white-space:pre">        </span>case .purchased?:</div><div class=""><span class="Apple-tab-span" style="white-space:pre">                </span>…</div><div class=""><span class="Apple-tab-span" style="white-space:pre">        </span>case .restored?:</div><div class=""><span class="Apple-tab-span" style="white-space:pre">                </span>…</div><div class=""><span class="Apple-tab-span" style="white-space:pre">        </span>case .error?:</div><div class=""><span class="Apple-tab-span" style="white-space:pre">                </span>…</div><div class=""><span class="Apple-tab-span" style="white-space:pre">        </span>case nil:</div><div class=""><span class="Apple-tab-span" style="white-space:pre">                </span>// Handle unexpected states</div><div class=""><span class="Apple-tab-span" style="white-space:pre">        </span>}</div><div class=""><br class=""></div><div class="">And then when you added a new case in iOS 8, you could say this, and everyone's code would run through the `nil` path:</div><div class=""><br class=""></div><div class=""><div class=""><span class="Apple-tab-span" style="white-space: pre;">        </span>@available(iOS 3.0)</div><div class=""><span class="Apple-tab-span" style="white-space: pre;">        </span>public enum&nbsp;PaymentTransactionState&nbsp;{</div><div class=""><span class="Apple-tab-span" style="white-space: pre;">                </span>case purchasing</div><div class=""><span class="Apple-tab-span" style="white-space: pre;">                </span>case purchased(Purchase)</div><div class=""><span class="Apple-tab-span" style="white-space: pre;">                </span>case restored(Purchase)</div><div class=""><span class="Apple-tab-span" style="white-space: pre;">                </span>case error(Error)</div><div class=""><br class=""></div><div class=""><span class="Apple-tab-span" style="white-space: pre;">                </span>@available(iOS 8.0)</div><div class=""><span class="Apple-tab-span" style="white-space: pre;">                </span>case deferred</div></div><div class=""><span class="Apple-tab-span" style="white-space:pre">                </span></div><div class=""><span class="Apple-tab-span" style="white-space:pre">                </span>switch -&gt; PaymentTransactionState? {</div><div class=""><span class="Apple-tab-span" style="white-space:pre">                        </span>return nil</div><div class=""><span class="Apple-tab-span" style="white-space:pre">                </span>}</div><div class=""><span class="Apple-tab-span" style="white-space:pre">        </span>}</div><div class=""><br class=""></div><div class="">An alternative design would have been to add a `case other` from the start, anticipating that future versions would need to map unknown cases to that one. (Or you could specify `switch -&gt; Never` to forbid switching entirely, or perhaps we could let you say `switch throws` to require the user to say `try switch`. But you get the idea.)</div><div class=""><br class=""></div><div class="">Finally, the kicker: If you do *not* specify an `exhaustive` block, then it is treated as though you had written `switch -&gt; Self? { return nil }`. That is, a "non-exhaustive" enum is just one which turns into an optional when you switch over it, and returns `nil` for unknown cases. Thus, there basically *are* no unknown cases.</div><div class=""><br class=""></div><div class="">Implementation-wise, I imagine that when switching over an enum from `public`, you'd need to make a call which took a version parameter and returned a value compatible with that version. (This might need to be some sort of table of versions, depending on how we end up extending @available to support versions for arbitrary modules.)</div><div class=""><br class=""></div><div class=""><span class="Apple-tab-span" style="white-space:pre">        </span>* * *</div><div class=""><br class=""></div><div class="">As for the "untestable code path" problem…maybe we could let you mark certain enum parameters as `@testable`, and then, when brought in through an `@testable import`, allow a `#invalid` value to be passed to those parameters.</div><div class=""><br class=""></div><div class=""><span class="Apple-tab-span" style="white-space:pre">        </span>// Library code</div><div class=""><span class="Apple-tab-span" style="white-space:pre">        </span>extension PurchasableItem {</div><div class=""><span class="Apple-tab-span" style="white-space:pre">                </span>func updateInventory(for state: @testable PaymentTransactionState, quantity: Int) throws {</div><div class=""><span class="Apple-tab-span" style="white-space:pre">                        </span>switch state {</div><div class=""><span class="Apple-tab-span" style="white-space:pre">                        </span>case .purchasing:</div><div class=""><span class="Apple-tab-span" style="white-space:pre">                                </span>return</div><div class=""><span class="Apple-tab-span" style="white-space:pre">                        </span>case .purchased, .restored:</div><div class=""><span class="Apple-tab-span" style="white-space:pre">                                </span>inventory += quantity</div><div class=""><span class="Apple-tab-span" style="white-space:pre">                        </span>case .failed(let error):</div><div class=""><span class="Apple-tab-span" style="white-space:pre">                                </span>throw error</div><div class=""><span class="Apple-tab-span" style="white-space:pre">                        </span>default:</div><div class=""><span class="Apple-tab-span" style="white-space:pre">                                </span>throw ProductError.unknownTransactionState</div><div class=""><span class="Apple-tab-span" style="white-space:pre">                        </span>}</div><div class=""><span class="Apple-tab-span" style="white-space:pre">                </span>}</div><div class=""><span class="Apple-tab-span" style="white-space:pre">        </span>}</div><div class=""><br class=""></div><div class=""><span class="Apple-tab-span" style="white-space:pre">        </span>// Test</div><div class=""><span class="Apple-tab-span" style="white-space:pre">        </span>func testUnknownTransactionState() {</div><div class=""><span class="Apple-tab-span" style="white-space:pre">                </span>XCTAssertThrowsError(myProduct.update(for: .#invalid) { error in</div><div class=""><span class="Apple-tab-span" style="white-space:pre">                        </span>XCTAssertEqual(error, ProductError.unknownTransactionState)</div><div class=""><span class="Apple-tab-span" style="white-space:pre">                </span>}</div><div class=""><span class="Apple-tab-span" style="white-space:pre">        </span>}</div><div class=""><br class=""></div><div class="">An `@testable` value could not be passed to a non-`@testable` parameter or into a non-`@testable` module, including the actual module the original type came from, unless you had somehow excluded the possibility of an `#invalid` value. You would need to design your code rather carefully to work around this constraint, but I think it could be done.</div><br class=""><div class="">
<span class="Apple-style-span" style="border-collapse: separate; font-variant-ligatures: normal; font-variant-east-asian: normal; font-variant-position: normal; line-height: normal; border-spacing: 0px;"><div class=""><div style="font-size: 12px; " class="">--&nbsp;</div><div style="font-size: 12px; " class="">Brent Royal-Gordon</div><div style="font-size: 12px; " class="">Architechies</div></div></span>

</div>
<br class=""></div>_______________________________________________<br class="">swift-evolution mailing list<br class=""><a href="mailto:swift-evolution@swift.org" class="">swift-evolution@swift.org</a><br class="">https://lists.swift.org/mailman/listinfo/swift-evolution<br class=""></div></blockquote></div><br class=""></div></body></html>