Handling unknown cases in enums [RE: SE-0192]

Notably, we don't use it for default, which this is a warning analogue of. Your argument could just as well be for renaming default to default case. The precedent argument works both ways, in this case.


I understand the clarity argument for unknown default or default(unknown), but those are too unwieldy for my liking. Some users may use this keyword prevalently to guard switches within their own projects, replacing hard errors with compiler warnings and runtime alerts.

Rather than gamble on usage being uncommon enough to justify a double-barrelled fileprivate-like name, it seems wise to follow the success of open, and use the unopinionated ‘unknown’.

I disliked "open" for the same reason as I now dislike naked "unknown": it's superficially clean, but it's also completely different from everything else that exists, which requires all users to learn from scratch.

Stripped of any relationship to what exists already, I worry it's just adding to the sense that Swift has an unstructured soup of new syntax: case! default! unknown! frequent! unlikely! up! down! sweet! bitter! umami!

Vladimir.S:

Can we have ‘unknown case’ in switch for ‘standard’(frozen) enum?

Yes, per suggestion by someone earlier (possibly you?) it will be okay to write unknown case-or-whatever for a switch
over an enum that has not been explicitly marked frozen, including all non-stdlib-non-overlay Swift enums. (The
compiler will not require it but it will allow it.) I’ll make sure that’s clear in the revised proposal.

Just to clarify: will it be allowed in both situations - when switch over 'standard' enum is 'exhaustive' and when that
switch 'non-exhaustive'?

If so, will it work at run time, if 'standard'(frozen) enum added new case somehow and sent to our code? (Can't
understand right now if this possible at all, but will compiler provide a support for this or code will crash if uknown
value from 'frozen' enum appeared in switch even with 'unknown case' branch specified?)

Then, such 'unknown case' IMO plays a role of 'default' more than role of some concrete 'case'.

I'm looking into a result of the poll right now, and seems like more people like just 'unknown:'.
But they should be aware, that this 'unknown:' branch could be called also for known cases, and so such name will
produce a high level of confusion IMO.

So, this branch('unknown case:'/'unknown:'/etc) is just a default in switch from compiled code's point of view. Its
difference from 'normal' default is just a warning for developer if it can be reached during the compilation time.
So conclusion is.. 'default(unknown):' is the right choice. At least I strongly believe so :-)

I'd like to mention something that seems to have gone unmentioned so far.

In that this new syntax is intended to affect compile-time diagnostics and not runtime behavior, it feels a lot like existing attributes in the language such as @discardableResult (or @warn_unqualified_access).

Might it be wise to explore something in that direction? Maybe something like @unused default?

The behavior would fall naturally out of the syntax:

  • If all known cases are switched over and default is written: “warning: ‘default’ unreachable in known cases; add ‘@unused’ to provide resilient default for unknown cases” (fixit to add attribute)

  • If @unused default is written switching over a frozen enum: “error: ‘@unused default’ unreachable because enum is frozen” (fixit to remove default)

  • If @unused default is written and is statically known to be used: “warning: ‘@unused default’ reachable in known case ‘.xyz’” (fixit to insert known case)

11 Likes

You make a good point, but unknown default has a similar problem. Making it an attribute is an improvement, but I'd prefer solving the problem through use of a clearer keyword, such as fallback.

As for the learning curve, attaching a redundant, familiar keyword is practical… But practical in the sense of fileprivate. Again, this could be used pervasively in a user's project, if they prefer warnings and runtime alerts.

Yet even there, “default” is syntactic sugar for “case _”.

• • •

In any case, it may turn out that you are right and “unknown” alone is best. Or perhaps Xiaodi is right and it should be an attribute. Or maybe something else should be chosen.

But the idea of unilaterally and preemptively attempting to remove “unknown case” from the discussion entirely, really rubs me the wrong way. Especially when the purported justification does not actually hold water.

Sorry if it came across that way. My intention was to note how that particular argument didn't appeal to me, not dismiss the whole idea.

I just had a literal shower-thought:

We’ve been talking about a warning in the specific situation of a switching over a non-frozen enum, but perhaps we should decouple the two.

The warning we want is, essentially, the inverse of an unreachable-code warning. If we had a way for developers to assert “this code should be unreachable” and raise a warning if it isn’t, then we could just use that and leave switches alone.

For example:

let transactionState: SKPaymentTransactionState = …

switch transactionState {
case purchasing: …
case purchased: …
case restored: …
case deferred: …
case failed: …
default: #warnIfReachable
  // show the user an alert or something
}
4 Likes

I like this thought. Do you have any ideas for how else this could be used?

Off the top of my head, I could imagine a debug constant could be made to warn when set, though that use case seems rather hacky.

edit: Thinking on it, any if-statement test can be inverted for an unreachable code warning…

It would be nice if this could build on the #warning proposal.

Unfortunately that’s what you get when you design by committee.

I actually really like fallback. It makes it clear what is going on, and it also makes it clear when it warns that “fallback will never be used” if the enum is exhaustive.

1 Like

+1, this being an attribute makes a LOT of sense to me.

-Chris

7 Likes

+1 for annotation

+1. Quote my old post:

Some ideas:

(silent | warn | ignore | ignorable) + (future | unknown) + [cases]

e.g.

  • @ignorableFutureCases
  • @ignorableUnknownCases
  • @warnFutureCases
  • @warnUnknownCases
  • @ignoreFuture
  • @ignoreUnknown
  • @silentUnknown

The thing I don't like about using an attribute modifier on default (like @unused default) is that it is more difficult to type then simply replacing default with another word. At least for me, I'll always use unknown instead of default for all non-frozen switches, so I worry it would become a bit annoying to add the attribute each time.

I would say the same thing about @escaping except that the compiler can insert it for you automatically via fix-its, so it's not an issue.

Having said that, my worries would probably go away if it will work with auto-complete once the '@' has been typed.

1 Like

Yes, seems like attribute is better alternative. And for now '@unused default' is better candidate IMO

But I want to point out that this Xiaodi's statement seems is not true, IIUC:

If @unused default is written switching over a frozen enum: “error: ‘@unused default’ unreachable because enum is
frozen” (fixit to remove default)

From Jordan Rose's replies I made a conclusion that '@unused default'(aka 'unknown case') will be allowed for
'standard'(frozen) enums.
Moreover, in this case there will be a nice symmetry:

// 'standard'(aka frozen) enum

enum MyEnum { case one, two }

// 1. Exhaustive switch with 'default'

switch myenum {
case .one : ...
case .two : ...
default : ... // Current behavior: Warning: Default will never be executed
// Will be addedd: .. Add @unused if expected
}

// 2. Exhaustive switch with '@unused default'

switch myenum {
case .one : ...
case .two : ...
@unused default : ... // No warnings, we expect this will not be called,
// until MyEnum added new case. And after this
// we can still compile the code(with warning).
// I.e. don't have to fix the problem right now - for example when
// MyEnum is declared in 3rd party source file, not our own file.
}

// 3. Non-exhaustive switch with 'default' - standard situation, no warnings

// 4. Non-exhaustive switch with '@unused default'

switch myenum {
case .one : ...
@unused default : ... // Warning: @unused default will be executed. Remove @unused attribute to silence the warning.
}

1 Like

As far as names go, "unknown" is the "least bad" term that's been suggested so far (separate from whether it's a modifier, or an attribute, or a standalone thing). I'll admit that part of that is because it's not particularly "content-ful", i.e. it doesn't have an immediate existing meaning and isn't a technical term related to anything else. But the problem is inherently one with two parts—non-public cases and future cases—and we haven't come up with a better term that encompasses both.

(The obvious thing to try is to not encompass both, but unfortunately I can't think of how the compiler could possibly differentiate them, especially when dealing with C enums.)

On some specific other names I've seen coming up in this thread:

  • unused - The case is not unused. It will be used if the library developer adds a new enum case.
  • fallback - Not bad, but I'm concerned that as a word it's not really different from default.

To the idea of using #warning or #warnIfReachable rather than a dedicated annotation, please remember that this code is reachable. The assumption is that future cases will be added—it has to be to produce a correct program (and to not accidentally optimize away the catch-all case).


An attribute on the switch isn't inherently bad, but I don't see how it's better than an annotation on the case (with whatever spelling).


To Vladimir's last point, the proposal I'm writing up draws a distinction between "implicitly frozen" enums (the ones users define) and "explicitly frozen" enums (the ones in libraries). Using a catch-all case in an otherwise exhaustive switch will produce a warning on explicitly frozen enums, but unknown will be allowed for implicitly frozen enums. I suspect this will not affect anyone in practice; it just happens to do the right thing in the right places.

@default:

fallback is unfortunate because it's very close in appearance to fallthrough, and both will be mixed together in switch statements.

3 Likes

To the idea of using |#warning| or |#warnIfReachable| rather than a dedicated annotation, please remember that this code
/is reachable.

Well... isn't something like @unreachableAtCompiletime or @runtimeReachableOnly or even @runtimeOnly is the right
direction?