<html><head><meta http-equiv="content-type" content="text/html; charset=utf-8"></head><body dir="auto"><div><br><br>Sent from my iPad</div><div><br>On Aug 9, 2017, at 12:15 PM, Tony Allevato via swift-evolution &lt;<a href="mailto:swift-evolution@swift.org">swift-evolution@swift.org</a>&gt; wrote:<br><br></div><blockquote type="cite"><div><div dir="ltr"><div class="gmail_quote"><div dir="ltr">On Wed, Aug 9, 2017 at 9:40 AM David Sweeris via swift-evolution &lt;<a href="mailto:swift-evolution@swift.org">swift-evolution@swift.org</a>&gt; wrote:<br></div><blockquote class="gmail_quote" style="margin:0 0 0 .8ex;border-left:1px #ccc solid;padding-left:1ex"><div dir="auto"><div>(Now with more mailing lists in the "to" field!)</div><div><div></div></div></div><div dir="auto"><div><div><div>On Aug 8, 2017, at 3:27 PM, Jordan Rose via swift-evolution &lt;<a href="mailto:swift-evolution@swift.org" target="_blank">swift-evolution@swift.org</a>&gt; wrote:<br><br></div><blockquote type="cite"><div><div dir="auto" style="word-wrap:break-word;line-break:after-white-space">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:<div><br></div><div>- 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.</div><div>- 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?</div><div>- Adding a new case to a&nbsp;<i>Swift</i>&nbsp;enum in a library breaks any client code that was trying to switch over it.</div><div><br></div><div>(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.)</div><div><br></div><div>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&nbsp;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&nbsp;Strings in it.</div><div><br></div><div><br></div><div><b>Behavior</b><br><br>I think there's certain behavior that is probably not&nbsp;<i>terribly</i>&nbsp;controversial:<br><br>- 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&nbsp;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.)<br>- When I define Swift enums in the current framework, there's obviously no compatibility issues; we should allow exhaustive switches.<br><br>Everything else falls somewhere in the middle, both for enums defined in Objective-C:<br><br>- 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&nbsp;still be private cases defined in a .m file?<br>- If there's an Objective-C enum in&nbsp;<i>another</i>&nbsp;framework (that I built locally with Xcode, Carthage, CocoaPods, SwiftPM, etc.), should it allow exhaustive switching, because&nbsp;there are no&nbsp;<i>binary</i>&nbsp;compatibility issues, or not, because there may be&nbsp;<i>source</i>&nbsp;compatibility issues? We'd really like adding a new enum case to&nbsp;<i>not</i>&nbsp;be a&nbsp;breaking change even at the source level.<br>- 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&nbsp;might be non-modular content I've used the bridging header to import?<br><br>And in Swift:<br><br>- 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&nbsp;may be source compatibility issues? Again, we'd really like adding a new enum case to&nbsp;<i>not</i>&nbsp;be a breaking change even at the source level.<br><br></div><div>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&nbsp;you&nbsp;must&nbsp;have a 'default' in a switch". In previous (in-person) discussions&nbsp;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&nbsp;also&nbsp;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&nbsp;anyway. (Think about Apple's frameworks again.) I don't have a great answer, though.<br><br>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&nbsp;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.<br><br></div><div><br></div><div><b>Terminology</b></div><div><b><br></b></div><div>The "<a href="http://jrose-apple.github.io/swift-library-evolution/" target="_blank">Library Evolution</a>" 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:</div><div><br></div><div>- For classes, "open" and "non-open" restrict what the <i>client</i> can do. For enums, it's more about providing the client with additional guarantees—and "non-open" is the one with more guarantees.</div><div>- 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".</div><div><br></div><div>That said, Clang now has an 'enum_extensibility' attribute that does take 'open' or 'closed' as an argument.</div><div><br></div><div>On Matthew's thread, a few other possible names came up, though mostly only for the "closed" case:</div><div><br></div><div>- '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.</div><div>- 'locked': reasonable, but not a standard term, and could get confused with the concurrency concept</div><div>- 'exhaustive': matches how we've been explaining it (with an "exhaustive switch"), but it's not exactly the <i>enum</i>&nbsp;that's exhaustive, and it's a long keyword to actually write in source.</div><div><br></div><div>- 'extensible': matches the Clang attribute, but also long</div><div><br></div><div><br></div><div>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 <i>really like to find some</i>.</div><div><br></div><div><br></div><div><b>Proposal</b></div><div><b><br></b></div><div>Just to have something to work off of, I propose the following:</div><div><br></div><div>1. All enums (NS_ENUMs) imported from Objective-C are "open" unless they are declared "non-open" in some way (likely using the&nbsp;enum_extensibility attribute mentioned above).</div><div>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&nbsp;an app, where binary compatibility is a concern.<br>3. All public Swift enums in modules compiled from source have the option to be either "open" or "closed".</div><div>4. In Swift 5 mode, a public enum should be <i>required</i> 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".)</div><div>5. None of this affects non-public enums.</div><div><br></div><div>(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.</div><div><br></div><div><br></div><div><b>Why now?</b></div><div><br></div><div>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 <i>have</i>&nbsp;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".</div><div><br></div><div>I've written quite enough at this point. Looking forward to feedback!</div></div></div></blockquote><br></div></div></div><div dir="auto"><div><div><div>How does this compare with the other idea (I can't remember who posted it) of allowing enum "subtyping"?</div><div>enum Foo {</div><div>&nbsp; case one</div><div>&nbsp; case two</div><div>}</div><div>enum Bar : Foo {</div><div>&nbsp; // implicitly has Foo's cases, too</div><div>&nbsp; case three</div><div>}</div><div><br></div><div>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.</div></div></div></div></blockquote><div><br></div><div>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.</div><div><br></div><div>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?)</div><div><br></div><div>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:</div><div><br></div><div>enum Foo {</div><div>&nbsp; case one</div><div>&nbsp; case two</div><div>}</div><div>// I'm not recommending this syntax, just writing it differently to avoid the subtyping confusion stemming from overloading the colon<br></div><div>enum NewFoo including Foo {<br></div><div>&nbsp; case three</div><div>}</div></div></div></div></blockquote><div><br></div><div><span style="background-color: rgba(255, 255, 255, 0);">I agree with your observations regarding syntax that matches class inheritance or protocol conformance. &nbsp;The syntax I have played with in the past looks like this:</span><div><span style="background-color: rgba(255, 255, 255, 0);"><br></span></div><div><div><span style="background-color: rgba(255, 255, 255, 0);">enum NewFoo {<br></span></div><div><span style="background-color: rgba(255, 255, 255, 0);">&nbsp; cases Foo</span></div><div><span style="background-color: rgba(255, 255, 255, 0);">&nbsp; case three</span></div><div><span style="background-color: rgba(255, 255, 255, 0);">}</span></div><div><span style="background-color: rgba(255, 255, 255, 0);"><br></span></div><div><span style="background-color: rgba(255, 255, 255, 0);">This syntax has the advantage of placing all case declarations side by side, including the embedded cases. &nbsp;It is also very similar to the closest workaround we have today (although without a formal subtype relationship):</span></div><div><span style="background-color: rgba(255, 255, 255, 0);"><br></span></div><div><div><span style="background-color: rgba(255, 255, 255, 0);">enum NewFoo {<br></span></div><div><span style="background-color: rgba(255, 255, 255, 0);">&nbsp; case foo(Foo)</span></div><div><span style="background-color: rgba(255, 255, 255, 0);">&nbsp; case three</span></div><div><br></div><div>&nbsp; // also a static var or func for each case of Foo used to create values</div><div><span style="background-color: rgba(255, 255, 255, 0);">}</span></div></div></div></div><br><blockquote type="cite"><div><div dir="ltr"><div class="gmail_quote"><div><br></div><div>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.</div><div>fooProducer() -&gt; Foo *cannot* be changed to fooProducer() -&gt; NewFoo without breaking clients because the client is expecting a Foo, but not all NewFoos are Foos.</div><div><br></div><blockquote class="gmail_quote" style="margin:0 0 0 .8ex;border-left:1px #ccc solid;padding-left:1ex"><div dir="auto"><div><div><div><br></div><div>I don't know how libraries would deal with adding cases... maybe have different function signatures based on the version setting?</div></div><div><br></div><div>- Dave Sweeris</div></div></div>_______________________________________________<br>
swift-evolution mailing list<br>
<a href="mailto:swift-evolution@swift.org" target="_blank">swift-evolution@swift.org</a><br>
<a href="https://lists.swift.org/mailman/listinfo/swift-evolution" rel="noreferrer" target="_blank">https://lists.swift.org/mailman/listinfo/swift-evolution</a><br>
</blockquote></div></div>
</div></blockquote><blockquote type="cite"><div><span>_______________________________________________</span><br><span>swift-evolution mailing list</span><br><span><a href="mailto:swift-evolution@swift.org">swift-evolution@swift.org</a></span><br><span><a href="https://lists.swift.org/mailman/listinfo/swift-evolution">https://lists.swift.org/mailman/listinfo/swift-evolution</a></span><br></div></blockquote></body></html>