On 11.08.2017 2:37, Jordan Rose wrote:
Both you and Vladimir are bringing up this point, with Vladimir explicitly suggesting
a "future" case that's different from "default". Again, the pushback we get here is
that the "future" case is untestable…but maybe that's still an option worth having.
I wonder, how the 'default' in exhaustive switch on open enum is testable?
I mean, let's say we have such enum in one of the frameworks:
open enum MyOpenEnum {
case one
case two
}
, then in our code:
switch myOpenEnumInstance {
case .one : ...
case .two : ...
default : ... // how this can be tested?
}
I just strongly feel that be able to keep switch exhaustive at the moment of
compilation - is critical requirement in some cases, when it is very important to not
forget to process some cases. With just 'default' in switch for open enum - we are
loosing this compiler's help. This is why 'future' case is required for open enums.
Also, if I understand correctly, we are going to have most of all extern(imported)
enums as 'open'. So, we are loosing the feature to receive a help for exhaustive
switch from compiler for most of such enums.
Moreover, shouldn't we just say that enums, that we have no Swift sources for at the
moment of compilation - should *always* be treated as 'open'? If we compile our
'switch' together with the source of switched enum - such enum can not be changed in
the future.
But, if enum is coming from other framework - we have no any control over it, and
even author of framework IMO can't be sure in most cases that enum will not be
extended in future, and probably we even should not ask author of framework to
consider its enum closed, as most likely one can't foresee for sure.
Wouldn't this be a simpler and more robust model to think about enums in Swift?
So you know, that *any* enum coming from framework(not from source) - can be changed
in future, and so you have to process this case explicitly. In this case we don't
need to mark enum as 'open' or 'closed' at all, but for some rare cases, when author
of framework *really sure* enum can not be changed in future(and future change in
enum will break all the code depending on it), we can introduce some 'final'
marker(or @exhaustive directive) to help compiler's optimizations.
Btw, is open enum is allowed to change the type of associated value for some cases or
even enum's raw type? I.e. what changes in open enum will lead to crash in our code
and which will just be processed in 'default'/'future' block in switch?
Vladimir. (P.S. Sorry for long reply)
(At the very least, it's worth recording in any eventual proposal why we /don't/ have
it, and it could be added later if it turns out that was wrong.)
Thank you both for pushing on it.
Jordan
On Aug 9, 2017, at 21:55, Charlie Monroe <char...@charliemonroe.net
<mailto:char...@charliemonroe.net>> wrote:
Hi Jordan,
let's say I'm writing my custom number formatter and I switch
over NSNumberFormatterStyle (NumberFormatter.Style in Swift) - the question always
is what to do with the default case - it's a value that I am not programmatically
counting with. I would personally just put in fatalError with a description that it
was passed an unhandled style. Listing all enums in Foundation, I can see using
most of them this way.
I personally have most of my switches exhaustive, mainly for the sake of being
warned/error'ed when a new case is introduced - I've just done a quick search
through my projects and I use default: usually for switching over non-enums
(strings, object matching, ints, ...).
Maybe I'm in the minority here... Seemed like a good practice to me - usually the
enum doesn't have but a few items on the list and you usually don't handle just 1-2
cases in your switch, which makes the default label save you 1-2 lines of code that
can save you from unnnecessarily crashing during runtime...
On Aug 10, 2017, at 1:57 AM, Jordan Rose <jordan_r...@apple.com
<mailto:jordan_r...@apple.com>> wrote:
Hi, Charlie. This is fair—if you're switching over an open enum at all, presumably
you have a reason for doing so and therefore might want to handle all known cases
every time you update your SDK. However, I think in practice that's going to be
rare—do you have examples of exhaustive switches on SDK enums that exist in your
own app?
(There's an additional piece about how to handle cases with different
availability—there's nowhere obvious to write the #available.)
I suspect marking SDK enums "closed" will be much easier than nullability, simply
because there are so few of them. Here's some data to that effect: out of all 60
or so NS_ENUMs in Foundation, only 6 of them are ones I would definitely mark
"closed":
- NSComparisonResult
- NSKeyValueChange / NSKeyValueSetMutationKind
- NSRectEdge
- NSURLRelationship
- /maybe/ NSCalculationError
There are a few more, like NSURLHandleStatus, where I could see someone wanting to
exhaustively switch as well, but the main point is that there /is/ a clear default
for public enums, at least in Objective-C, and that even in a large framework it's
not too hard to look at /all/ of them.
(Note that NSComparisonResult is technically not part of Foundation; it lives in
the ObjectiveC module as /usr/include/objc/NSObject.h.)
Jordan
On Aug 8, 2017, at 21:53, Charlie Monroe <char...@charliemonroe.net
<mailto:char...@charliemonroe.net>> wrote:
While I agree with the entire idea and would actually use behavior like this in a
few instances, I feel that in most cases, you would simply put
default:
fatalError()
The huge downside of this is that you no longer get warned by the compiler that
you are missing a case that was added - a common thing I personally do (and I
have a feeling I'm not alone) - add an enum case, build the app, see what broke
and fix it - as you get warnings/errors about the switch not being exhaustive.
You find this out during runtime (if you're lucky), otherwise your end user.
As you've noted all enums from ObjC would need to be marked with an annotation
marking if they are closed - which given the way nullability is still missing in
many frameworks out there, I think would take years.
I'd personally expand this proposal by introducing switch! (with the exclamation
mark) which would allow to treat open enums as closed. Example:
// Imported from ObjC
open enum NSAlert.Style { ... }
switch! alert.style {
case .warning:
// ...
case .informational:
// ...
case .critical:
// ...
}
The force-switch would implicitely create the default label crashing, logging the
rawValue of the enum.
Thoughts?
On Aug 9, 2017, at 12:28 AM, Jordan Rose via swift-evolution
<swift-evolution@swift.org <mailto:swift-evolution@swift.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
than 'final' on a class, which is a restriction on code elsewhere in the same
module.
- 'locked': reasonable, but not a standard term, and could get confused with the
concurrency concept
- 'exhaustive': matches how we've been explaining it (with an "exhaustive
switch"), but it's not exactly the /enum/ that's exhaustive, and it's a long
keyword to actually write in source.
- 'extensible': matches the Clang attribute, but also long
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 /really like to find some/.
*Proposal*
*
*
Just to have something to work off of, I propose the following:
1. All enums (NS_ENUMs) imported from Objective-C are "open" unless they are
declared "non-open" in some way (likely using the enum_extensibility attribute
mentioned above).
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 an app, where binary compatibility is a concern.
3. All public Swift enums in modules compiled from source have the option to be
either "open" or "closed".
4. In Swift 5 mode, a public enum should be /required/ 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".)
5. None of this affects non-public enums.
(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.
*Why now?*
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 /have/ 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".
I've written quite enough at this point. Looking forward to feedback!
Jordan
_______________________________________________
swift-evolution mailing list
swift-evolution@swift.org <mailto:swift-evolution@swift.org>
https://lists.swift.org/mailman/listinfo/swift-evolution
_______________________________________________
swift-evolution mailing list
swift-evolution@swift.org
https://lists.swift.org/mailman/listinfo/swift-evolution