Allow `@unknown default` on all enum switching to enforce default case handling

Yes, thank you.

Not quite:

  1. For types for which exhaustiveness checking is impossible, they would be newly equivalent (@unknown currently disallowed)
  2. For types for which exhaustiveness checking is possible, default would still be the way to spell "I am knowingly deciding to lump all remaining cases (present and future) together"

Objc/Clang has a compiler flag that just does this: -Wswitch-enum. In this way Objc can be safer, product logic wise, than Swift.

I'm not saying expanding @unknown default everywhere is the best solution, but given Swift doesn't like dialects nor breaking changes, facilitating a linter with this non-breaking change is one option.

There's a lot of hold up about what @uknown default means on a string/int switch, but it's really rather arbitrary that we use default today and ban @unknown default. In the space of infinity, the values that are "knowable" is the same as the values that are "unknowable."

I think I'm seeing the outline of what you're getting at.

I think perhaps we're using terminology a little differently. I think of nonexhaustive enums as types for which exhaustiveness checking is impossible by construction, and these not only allow but require considering the @unknown case in an exhaustive switch.

But I take it you're speaking of those situations for which default is (for whatever reason) always required in an exhaustive switch because current Swift is unable to, or not trying to, check exhaustiveness—which includes fixed-width integers of any size (even [U]Int8, which is easily exhaustively switched over) and, as I recall, still includes (or at least did until recently) Optional<Bool> values.

I have often thought we'd want to be able to spell something like @unreachable default for such types: For example, if I switch over an integer x and I have one case handling x > 0 and another handling x <= 0, I should be able to tell the compiler, "I know you didn't check for exhaustiveness, but I'm telling you that I did, and this default is for you and not for me." Perhaps such an annotation would help here if the compiler smartens up and can prove that the default case is reachable?

But I'm still having trouble wrapping my head around why it would be good deliberately to allow known values to be handled by @unknown default specifically in those situations. That seems like we're inviting more trouble and not less. For example, if I switch over an Optional<Bool> value y, but I only handle case true, why should I then be allowed to group false and nil under @unknown default?

I don't think it's arbitrary at all: @unknown values denote states that literally do not exist and are unutterable at the time of compilation, and I don't understand the notion that many values existing and utterable—even infinitely many—make them non-existent. If Elon has so much money that he can't count it all, it doesn't mean that he might as well have no money.

3 Likes

I think you're coming at this from the wrong direction. The goal as I understand it here is, first and foremost, "ban the use of default in my codebase". But if this is adopted as a hard-line policy it also amounts to "never switch over anything which is non-exhaustive". A carveout likely must be made for resilient enums anyway, to allow for @unknown default, and so the request is to allow for an 'escape hatch' when switching over a structurally non-exhaustive value (such as an Int or a String), and just permit that resilient enum carveout to apply to other non-exhaustive values as well.

ETA: In other words, "allow @unknown default for non-exhaustive values" is not being held up here as an ideal policy:

Rather, it's a means to an end of "give me some way to ban default for exhaustive values without completely disallowing switch on non-exhaustive values".

5 Likes

Ahhhhh. Let me see if I can articulate the actual requested feature correctly:

  • If a user writes a default case when in fact @unknown default would suffice to make the switch exhaustive, we want an option for the compiler to tell a user: hey, you already got all the known cases, and you shouldn't write a naked default here because you won't be informed that you're missing a known case in the future and that's bad.

  • If a user writes a default case when in fact @unknown default plus any number of compiler-enumerable concrete known cases would suffice to make the switch exhaustive, we want an option for the compiler to tell a user: hey, you forgot about these n known cases today, and after you've resolved that, you shouldn't write a naked default here because you won't be informed that you're missing a known case in the future and that's bad.

  • If the solution to delivering the two features above is requiring users to write @unknown default everywhere, then either the language must allow things that are known to be called unknown or else we can't switch over anything except exhaustive enums and Bool.

If we haven't already, I think we could just make the first bullet point an across-the-board warning as a diagnostics QoL thing: it is completely straightforward to silence.

The second bullet point probably could be an opt-in diagnostic with Evolution review: the consequence of enabling that option would be a more stringent subset-not-dialect of the language (since anything that doesn't trigger the opt-in diagnostic would obviously be perfectly valid vanilla Swift). We could even make it configurable: warn me about this issue if I can write out fewer than n additional cases, but more than that (even if the compiler knows how many) leave me be.

We could also explore whether, if n = 1 (i.e., replace default with case .foo, adding @unknown default if needed), this should always be enabled.

This obviates any need for reckoning with the third bullet point.

3 Likes