But all these fall into the category of you have a broadly applicable default, option 3, and therefore you would use a non-exhaustive switch with mandatory default .
So do you have an example that is not options 1, 2, or 3?
Right. A switch over a non-frozen enum requires a default. That is correct.
• • •
Now, having added a default to your switch, the enum gets updated with new cases.
The switch continues to work, sending the new case to the default, exactly as intended.
• • •
And you, as the maintainer of the code containing the switch, want to update the switch to handle the new cases differently.
Or at least, you *would* want to update the switch, if you had any idea whatsoever that those new cases exist. But you don’t, because nobody has time to read detailed documentation on every update to every module they use.
And even if you *did* know new cases were added, you’d still have to find every place in your code with a switch over that enum, which is tedious at best and it’s easy to miss a spot.
• • •
Wouldn’t it be a whole lot nicer if the switches were marked, and the compiler informed you when new cases were added and where you switch over them?
class SimpleEnum {
static let one = SimpleEnum()
static let two = SimpleEnum()
private init()
}
And then you can use pointer identity checking instead of needing a private payload.
With ComplexEnum, you'd use a static func instead of a static let, but you run in to problems with defining how the payload is stored and how to do case-let syntax.
But I agree in principle with @Nevin: generally structs with static members are a far superior way to express what people typically use enums for.
Static properties are the way Java does enums and it does work well. Java also allows payloads and has some nice syntax sugar. Generally you would call a method on the case instead of inside a method switching, this works well when new cases are added since normal OO dynamic dispatch takes care of things for you.
Why would the compiler not warn you? If you use switch! or switch? it is an error. If you want a warning from switch with default then that will come with version locking. Or a special case of version locking could be added now for enums to generate a warning.
But be careful about what you wish for. Either you have a generally, broadly applicable default that is highly likely to work for any new case or you don't. If you don't have a broadly applicable default then you should be using switch! or switch?. Consider these two scenarios with unknown:
// Scenario 1.
enum Errors { case A, B, C }
switch error {
case A: // Handle specific error A.
case B: // Handle specific error B.
case C: // Handle specific error C.
unknown case: // Generic handler.
}
Note that the switch is exhaustive with an unknown, now the generic handler wasn't good enough for any of the known cases at all. Yet it is going to be good enough for an unknown case - come off it. This is just a source of bugs.
// Scenario 2.
switch error {
case A: // Handle specific error A.
default: // Generic handler.
unknown case: // Generic handler.
}
The argument is that this is better than just a default since the compiler can flag a warning about new cases. But what ever warning mechanism that is adopted for this is equally applicable to:
switch error {
case A: // Handle specific error A.
default: // Generic handler.
}
So what has unknown case given us - a source of bugs?
Coming back to this discussion after a good 40 posts. Hope you haven't been feeling like I've been ignoring you all!
I think this has been a good exploration of the space of the problem. SE-0192 is an exercise in balances—library authors vs. client developers, C enums vs. Swift enums, best possible syntax vs. language source compatibility, "purity" vs. familiarity vs. avoiding API design traps, forward compatibility vs. exhaustiveness checking. What's proposed is my attempt at the balancing of these concerns, informed of course by the discussion that everyone's been contributing.
For the skepticism around unknown case in particular (as opposed to just having default), the first run of this proposal immediately got pushback for not being able to know when the library you're using has added a new case. Based on that response, I don't think the community would accept a proposal that doesn't have those diagnostics in some form; for the API compatibility side of things, such a diagnostic must be a warning and not an error.
switch! and switch? aren't totally unreasonable, but I think it's perfectly valid to want to do something else. At the very least, maybe you do some additional logging or state cleanup before you call fatalError. I'd rather not spend any more time talking about those.
Thanks, I think the proposal is well written, and hopefully even those who disagree with it will find it takes their concerns seriously.
(If Swift were younger, perhaps we would consider using protocols for all non-imported enums, not just non-frozen ones. But at this point that would be way too big a change to the language.)
Should this be “imported enums” not “non-imported enums”?
Thanks for all the work on all the revisions to the proposals. I think we’ve really hit a sweet spot now. I have have one remaining minor gripe:
A bigger change would be to make a custom pattern instead of a custom case, even if it were subject to the same restrictions in implementation (see "unknown case patterns" above). This usually meant using a symbol of some kind to distinguish the "unknown" from a normal label or pattern, leading to case #unknown or similar. This makes the new feature less special, since it's "just another pattern". However, it would be surprising to have such a pattern but keep the restrictions described in this proposal; thus, it would only make sense to do this if we were going to implement fully general pattern-matching for this feature. See "unknown case patterns" above for more discussion.
Wouldnt it make sense to name the unknown case case #unknown or simply #unknown so that if we do end up implementing more in-depth pattern matching, we’re already set with a nicer keyword? I’m really not a fan of ˋunknown caseˋ ending up in pattern matching (if we ever get there).
I've been following this proposal for a long time, and was in the "adding new enum elements should be a source breaking change" camp for much of it.
However, I now tend to think that a warning is fine. If you really care about fixing potential bugs due to new cases being added, you are probably already the kind of person that obsesses over whether your code compiles with no warnings (I'm one of these people), so a warning suffices to tell you where to go fixing the code.
Sometimes you really do just want to hack and test if your project builds/runs with some new version of a library (or multiple libraries), and someone will have already written some code in the unknown case: handler to do something (hopefully) not-totally-unreasonable at runtime. Now sure, that unknown case could be a logic bomb waiting to erase your hard drive because it was never actually executed or tested before, but it's unclear to me what proportion of unknown cases in the wild are going to be of this nature. It probably depends on the type of code/systems you are working on and I think this probably explains the difference of opinions.
What would be nice is to be able to have fine-grained control over treating some warnings as errors (-Werror-foo style flags) but I understand the Swift team is reluctant to go down this route.
I think “unknown case” reads better than “unknown default” at the point of use. After all it isn’t the default that’s unknown, it’s the case that’s unknown.
This is why I suggested at one point default unknown: the rules surrounding this are rather more similar to default, so I like that the term is used, but unknown default does sound like we have an unknown default, which isn't the idea.
Well, it functions as a default in practice, but that’s almost incidental. Conceptually it exists to handle unknown cases, and calling it “unknown case” both expresses its purpose and makes it obvious why a warning is issued when a known case would reach it.
Would it be possible for case to be implicit, like it is on default?
A switch matches against a list of cases. It would be appropriate for unknown to work like default in the sense that the word “case” is implicit.
switch myEnum {
case .a: ...
case .b: ...
default: ...
unknown: ...
}
Order is important too. Since unknown would be the most exceptional case, it would make more sense to code readers for it to be at the end of the list of cases: “Nothing else worked, not even default, as this case was introduced after this code was compiled”.
I don't really like the word 'unknown' at all. Given that unknown really is a default with some special diagnostic notes, I think keeping it close to default is good. It's not a very strong guarantee of different behaviour - it literally is just a default with a compile-time warning to help you maintain updates. It's not worth introducing a new keyword for, which people will need to learn, and it would make it more difficult to communicate that it's really just a minor tweak to the default you know and love today.
How about #default, where the # denotes that you intend the rest of the switch to be exhaustive?
Taking the example from above:
switch myEnum {
case .a: ...
case .b: ...
default: ... // Regular default. No warning if rest of the switch doesn't handle some cases.
#default: ... // Special default. The rest of the switch must handle all visible cases or get a warning.
}
I really don't think it needs to be more complex than that.
I'd like a simple unknown too. I think the reason is that it's ambiguous with existing syntax where you can give a label to a loop:
switch myEnum {
case .a:
var i = foo()
unknown: while i < 0 {
while i < 5 {
i -= 4
if i == 100 {
break unknown
} else if i == 101 {
continue unknown
}
}
i -= 1
}
}
Code like the above would stop compiling if unknown became a valid case because break and continue would fail to find their label. What's worse though is that if there was an unknown label but that label was not being referenced by a break or continue, it'd suddenly compile as two separate cases instead of one.
Perhaps there's no code in the universe that would break in this way, but we will never be sure of that.
Using unknown as a label wouldn't be a problem, as users would be able to use backticks to escape it, the same way backticks can be used on default, class, or any other language keyword. This could even be handled automatically by the Swift migration assistant in Xcode.
I'm not a big fan of "unknown" (case) either, but so far seems the better candidate.
Some alternatives (synonyms):
unrecognized <-- my favorite
unresolved <-- #2
undetermined
unidentified
uncertain
undisclosed
unrevealed
undecided
undefined <-- please don't :D
Whichever is the final keyword selected, it would be great if the word "case" was implied, as it is for the default case default:
I agree that the key concept here is default, just a special (limited) kind of default. Let me pitch my variant again, with an updated justification:
instead of #default call it default(unknown). It is similar to how private(set) limits the scope of private to set.
Then we will only have default, but it will get a limited variant default(unknown) that is only intended for unknown cases, similar in spirits to how private and private(set) relate to each other.
Also consider that having to explain a switch statement may either have a default or unknown case but not both makes it seem more complicated than it really is. The above variant justification simplifies understanding the rules and behavior.
Update: Note also that having unknown in parentheses means that it will not become a keyword.