<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 &quot;to&quot; 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&#39;d like to circle back to an issue that&#39;s been around for a while: the source compatibility of enums. Today, it&#39;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 &quot;private cases&quot; that aren&#39;t defined inside the original enum declaration, and there&#39;s no way to detect these in a switch without dropping down to the rawValue.</div><div>- For the same reason, the compiler-synthesized &#39;init(rawValue:)&#39; on an imported enum never produces &#39;nil&#39;, because who knows how anyone&#39;s using C enums anyway?</div><div>- Adding a new case to a <i>Swift</i> 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&#39;s because it&#39;s from a message of mine on a thread started by Matthew Johnson back in February called &quot;[Pitch] consistent public access modifiers&quot;. 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&#39;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.</div><div><br></div><div><br></div><div><b>Behavior</b><br><br>I think there&#39;s certain behavior that is probably not <i>terribly</i> 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&#39;s Apple&#39;s job to handle this and get it right, but if we get it wrong with an imported enum there&#39;s still the workaround of dropping down to the raw value.)<br>- When I define Swift enums in the current framework, there&#39;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 still be private cases defined in a .m file?<br>- If there&#39;s an Objective-C enum in <i>another</i> framework (that I built locally with Xcode, Carthage, CocoaPods, SwiftPM, etc.), should it allow exhaustive switching, because there are no <i>binary</i> compatibility issues, or not, because there may be <i>source</i> compatibility issues? We&#39;d really like adding a new enum case to <i>not</i> be a breaking change even at the source level.<br>- If there&#39;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&#39;ve used the bridging header to import?<br><br>And in Swift:<br><br>- If there&#39;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&#39;d really like adding a new enum case to <i>not</i> be a breaking change even at the source level.<br><br></div><div>Let&#39;s now flip this to the other side of the equation. I&#39;ve been talking about us disallowing exhaustive switching, i.e. &quot;if the enum might grow new cases you must have a &#39;default&#39; in a switch&quot;. In previous (in-person) discussions about this feature, it&#39;s been pointed out that the code in an otherwise-fully-covered switch is, by definition, unreachable, and therefore untestable. This also isn&#39;t a desirable situation to be in, but it&#39;s mitigated somewhat by the fact that there probably aren&#39;t many framework enums you should exhaustively switch over anyway. (Think about Apple&#39;s frameworks again.) I don&#39;t have a great answer, though.<br><br>For people who like exhaustive switches, we thought about adding a new kind of &#39;default&#39;—let&#39;s call it &#39;unknownCase&#39; 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&#39;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 &quot;<a href="http://jrose-apple.github.io/swift-library-evolution/" target="_blank">Library Evolution</a>&quot; doc (mostly written by me) originally called these &quot;open&quot; and &quot;closed&quot; enums (&quot;requires a default&quot; and &quot;allows exhaustive switching&quot;, respectively), but this predated the use of &#39;open&#39; to describe classes and class members. Matthew&#39;s original thread did suggest using &#39;open&#39; for enums as well, but I argued against that, for a few reasons:</div><div><br></div><div>- For classes, &quot;open&quot; and &quot;non-open&quot; restrict what the <i>client</i> can do. For enums, it&#39;s more about providing the client with additional guarantees—and &quot;non-open&quot; is the one with more guarantees.</div><div>- The &quot;safe&quot; default is backwards: a merely-public class can be made &#39;open&#39;, while an &#39;open&#39; class cannot be made non-open. Conversely, an &quot;open&quot; enum can be made &quot;closed&quot; (making default cases unnecessary), but a &quot;closed&quot; enum cannot be made &quot;open&quot;.</div><div><br></div><div>That said, Clang now has an &#39;enum_extensibility&#39; attribute that does take &#39;open&#39; or &#39;closed&#39; as an argument.</div><div><br></div><div>On Matthew&#39;s thread, a few other possible names came up, though mostly only for the &quot;closed&quot; case:</div><div><br></div><div>- &#39;final&#39;: has the right meaning abstractly, but again it behaves differently than &#39;final&#39; on a class, which is a restriction on code elsewhere in the same module.</div><div>- &#39;locked&#39;: reasonable, but not a standard term, and could get confused with the concurrency concept</div><div>- &#39;exhaustive&#39;: matches how we&#39;ve been explaining it (with an &quot;exhaustive switch&quot;), but it&#39;s not exactly the <i>enum</i> that&#39;s exhaustive, and it&#39;s a long keyword to actually write in source.</div><div><br></div><div>- &#39;extensible&#39;: matches the Clang attribute, but also long</div><div><br></div><div><br></div><div>I don&#39;t have better names than &quot;open&quot; and &quot;closed&quot;, so I&#39;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 &quot;open&quot; unless they are declared &quot;non-open&quot; in some way (likely using the enum_extensibility attribute mentioned above).</div><div>2. All public Swift enums in modules compiled &quot;with resilience&quot; (still to be designed) have the option to be either &quot;open&quot; or &quot;closed&quot;. This only applies to libraries not distributed with 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 &quot;open&quot; or &quot;closed&quot;.</div><div>4. In Swift 5 mode, a public enum should be <i>required</i> to declare if it is &quot;open&quot; or &quot;closed&quot;, so that it&#39;s a conscious decision on the part of the library author. (I&#39;m assuming we&#39;ll have a &quot;Swift 4 compatibility mode&quot; next year that would leave unannotated enums as &quot;closed&quot;.)</div><div>5. None of this affects non-public enums.</div><div><br></div><div>(4) is the controversial one, I expect. &quot;Open&quot; enums are by far the common case in Apple&#39;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 &quot;closed&quot;, it can be accessed more efficiently by a client. We don&#39;t <i>have</i> 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 &quot;open&quot; vs. &quot;closed&quot; was also the primary distinction between &quot;indirect access&quot; vs. &quot;direct access&quot;.</div><div><br></div><div>I&#39;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&#39;t remember who posted it) of allowing enum &quot;subtyping&quot;?</div><div>enum Foo {</div><div>  case one</div><div>  case two</div><div>}</div><div>enum Bar : Foo {</div><div>  // implicitly has Foo&#39;s cases, too</div><div>  case three</div><div>}</div><div><br></div><div>That way, if you switch over a `Foo`, you&#39;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&#39;s cases.</div></div></div></div></blockquote><div><br></div><div>It&#39;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&#39;t know enough about type theory to know whether that would be a problem or not. (Maybe it&#39;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>  case one</div><div>  case two</div><div>}</div><div>// I&#39;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>  case three</div><div>}</div><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&#39;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>