<div dir="ltr">On Tue, Aug 8, 2017 at 5:27 PM, Jordan Rose via swift-evolution <span dir="ltr">&lt;<a href="mailto:swift-evolution@swift.org" target="_blank">swift-evolution@swift.org</a>&gt;</span> wrote:<br><div class="gmail_extra"><div class="gmail_quote"><blockquote class="gmail_quote" style="margin:0 0 0 .8ex;border-left:1px #ccc solid;padding-left:1ex"><div style="word-wrap:break-word;line-break:after-white-space"><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><span class="HOEnZb"><font color="#888888"><div>Jordan</div></font></span></div></div></blockquote><div><br></div><div>Jordan, I&#39;m glad you&#39;re bringing this back up. I think it&#39;s clear that there&#39;s appetite for some forward movement in this area.</div><div><br></div><div>With respect to syntax--which the conversation in this thread has tackled first--I agree with the discussion that &quot;open&quot; and &quot;closed&quot; are attractive but also potentially confusing. As discussed in earlier threads, both &quot;open&quot; and &quot;closed&quot; will constrain the enum author and/or user in ways above and beyond &quot;public&quot; 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&#39;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).</div><div><br></div><div>I&#39;d like to suggest a different direction for syntax; I&#39;m putting it forward because I think the spelling itself naturally suggests a design as to which enums are (as you call it) &quot;open&quot; or &quot;closed,&quot; and how to migrate existing enums:</div><div><br></div><div>```</div><div>enum MyClosedEnum {</div><div>  case a</div><div>  case b</div><div>  case c</div><div>}</div><div><br></div><div>enum MyOpenEnum {</div><div>  case a</div><div>  case b</div><div>  case c</div><div>  default</div><div>}</div><div>```</div><div><br></div><div>In words, an enum that may have future cases will &quot;leave room&quot; 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.</div><div><br></div><div>Thoughts?</div><div><br></div><div><br></div></div></div></div>