SE-0192 β€” Non-Exhaustive Enums

I saw that; I just think that default(unknown) is a bit long.

As the proposal says, almost all non-trivial enums from a library will not be frozen, so lots of people are going to run in to this, including people brand-new to the language or writing their first iOS App. With that in mind I'd prefer something short.

I don't think it necessarily needs to be very explicit how it is different from a regular default. The rule that people will learn is that non-frozen enums always require some kind of default-handling.

1 Like

Yes, it is a bit long. But I still prefer it despite this deficiency.

Is there an elevator-pitch explanation on why default and unknown are mutually exclusive?

If that were true, why not just enforce default as required for non-frozen enums? Why is there a need to use a different keyword?

I think you should at least skim the (very long) discussion here. The point of unknown default (or whatever it ends up being called) is that developers are notified when new enum cases are added. You can use either with non-frozen enums, and they have different compile-time (but not run-time) behaviour.

1 Like

Apologies, but I tried, and also read the proposal again, and it was still not clear.

I guess the assumption is that RawRepresentable enums will always require default, so there is no need for unknown in those cases. Is this correct?

My question is, why not let the user mix both default and unknown, like this:

@nonFrozen enum Fruits {
  case apple
  case banana
}

switch fruit {
  case .apple: 
    print("apple")
  default: 
    print("not an apple. must be a banana!")
  unknown: 
    print("not an apple, nor a banana")
}

I was trying to understand the reasoning behind why default and unknown should not be allowed in the same switch statement.

PS: This is in relation to the requirement that default and unknown could not be used together, as stated by Hooman. Not sure if this was final or not, I was trying to clarify that point.

This section in the updated proposal might explain it.

1 Like

Ok, I see. Thank you for the link, I missed that.

I respectfully disagree. I think my example above shows that default and unknown are in essence different. default covers all scenarios known to the developer at the moment of writing code.

It is pretty common for developers to explicitly indicate only those cases that require special treatment, while covering other cases with default. Not necessarily error cases, but valid user flows.

unknown would be, however, an unexpected case, something that didn't exist at the time the code was written, and as such, it might be handled in a completely different way than the default case.

I believe both default and unknown should be allowed in the same switch statement.

unknown would be required by the compiler for @nonFrozen enums, while default would be required by the compiler for RawRepresentable enums (as it is on Swift 4).

I'm having trouble thinking of a good use case. The whole point of unknown is that it warns you at compile-time when new cases are added to the enum. If you have a default case in there as well then you will never get such a warning, and the new cases will be silently swallowed by the default when you recompile (as in the example from the proposal). So your default code needs to be general enough to handle these new cases, and you should probably just use default.

Your code will be much more robust, and make more sense, if you simply list the remaining currently-known cases that you want to handle in a single case and use unknown, instead of implicitly catching them with default. The compiler will verify that you've covered all known cases and you'll get a warning when new cases are added.

Note that unknown is not required for non-frozen enums. You can use default if you don't care about new cases.

My question is, how could an unknown that just prompts a build time warning possibly work? There’s no way for the build system to know I’m building against an updated version of a binary library. So when would this warning be triggered?

unknown is only supposed to be used for switches which you intend to be exhaustive. If the compiler can see any cases which you aren't explicitly handling, it will give you a warning.

It's exactly like default - but the compiler tries to help you keep your faux-exhaustive switches up-to-date.

1 Like

So it's more or less the same the following example?

// Your
switch something {
case ...:
case ...:
unknown case:
  ...
}

// Equals to
switch something {
case ...:
case ...:
default:
  #if some_known_cases_not_matched_yet 
    // 1. Compile time warning
    #warning("Some cases are not matched yet \(list_of_cases)")
  #endif
  // 2. Runtime warning for hidden cases
  #warning("Unknown case \(something)", at: .runtime) // Not supported directive, but I pitched the idea during the review
  ...
}

If I'm totally mistaken, then please feel free to correct me.

switch something {
case ...:
case ...:
unknown case:
  ...
}

is equivalent to:

switch something {
case ...:
case ...:
default:
  #warning("switch is not exhaustive")
  ...
}

Where, if all current cases are handled, the warning isn't shown.

It is a compile-time warning and nothing more. There is no need for excessive warnings (e.g. at runtime): cases which fall in to the default/unknown will be properly handled as a "default" case.

To throw a different name into the bucket, we can consider other instead:

switch non_frozen_enum_value {
case ...:
case ...:
other:
  ...
}

This is the shortest form so far, which matches the behavior from default and also signals that there must be more other than a simple duplicate keyword. Furthermore I don't think that's ambiguous with the existing syntax for labeled control-flow statements. Who on earth would call the label other? :D

To me, other sounds like it would do the same thing as default. It doesn't have the same connotation that something more like unknown does to me, i.e that it is intended only to handle cases the developer cannot know about at the time, for whatever reason.

1 Like

To be fair, default isn't better in that regard. It is however a suggestion from which the community then can choose. I'm not saying it's the best option, but as by now the shortest one.

There are other one that come to my mind:

  • divergent
  • mismatched
  • unused

I agree with the proposal in general. I don’t like the current syntax.

I prefer default unknown: or default(unknown):.

Also another possibility: What about something like @warnFutureCases (or @silentFutureCases) before switch statement and still use default?

1 Like

Hey, everyone. SE-0192 is not dead! Here's what's been going on.

Over the last few weeks I've been discussing this with the core team, who pretty much all agreed that this is not the right direction for third-party libraries. That is, for this table in the proposal:

Use Case Frozen Non-frozen
Multi-module app The desired behavior. Compiler can find all clients if the enum becomes non-frozen. Compiler can find all clients if the enum becomes frozen.
Open-source library (SwiftPM) Changing to non-frozen is a source-breaking change; it produces errors in any clients. Changing to frozen produces warnings in any clients.
ABI-stable library (Apple OSs) Cannot change to non-frozen; it would break binary compatibility. Changing to frozen produces warnings in clients (probably dependent on deployment target).

the first two rows are being unnecessarily inconvenienced by the frozen/non-frozen distinction.

This was always a controversial part of the proposal, since it meant that all enum that anyone had ever written in Swift would be affected. For C enums and the standard library, the benefit is clear, but for third-party libraries it was merely about whether a particular change (adding a case) would break source compatibility, and for an app that had just been broken up into multiple modules, there was almost no point at all. The core team, unlike me, did not feel that this trade-off was worth it. So, the frozen/non-frozen distinction will only apply to (1) C enums and (2) enums from the standard library and overlays.

This was the biggest part of the proposal, but the rest of it is mostly still relevant. In particular:

  • Switching over a C enum will require a catch-all clause, unless the enum is marked specially. (The current Clang attribute that controls that is spelled __attribute__((enum_extensibility(closed))).) This will be an error in Swift 5 mode, but we're still deciding whether it should be a warning in Swift 4 mode or just left un-diagnosed. (In either case, you'll be able to add the catch-all clause in Swift 4 mode without the compiler complaining.)

  • Switching over a Swift enum in the standard library or overlays will require a catch-all clause, unless that enum is marked specially. The attribute that distinguishes frozen enums from non-frozen enums will be underscored (something like @_frozen), indicating that it is not ready for general use yet; in order to discuss it properly, we need to be able to talk about the differences between "libraries with a stable binary interface" and "libraries without a stable binary interface", and SE-0192 is big enough already.

  • If an enum is explicitly marked frozen, using any sort of catch-all case will get an unreachable code warning, as it does today.

  • The community has made it clear that they want some form of catch-all case that will still result in compiler warnings whenever it's reachable with a known enum case. This is what I've been calling unknown case and what's being discussed over in Handling unknown cases in enums [RE: SE-0192] as well.

I'm working on cutting down the proposal text (and the implementation) to match this feedback from the core team, but there's also one more issue that needs to be worked out about unknown caseβ€”and no, I'm not just talking about the name. I'm going to bring that up in Handling unknown cases in enums [RE: SE-0192].

Once that's all done, the plan is to do another formal round of review (though likely a short one), since much of the community may not have had the stamina to keep up with these very long discussions. I may also start landing some of the implementation ahead of that just to keep from having to rebase it all the time (with approval from Ted), but all the checking will be behind a flag.

Thanks as always to everyone for their participation, patience, and thoughtful feedback.

18 Likes

Thanks very much for taking the time to work to this position. I very much appreciate the recognition that the initial approach would have caused much undue pain for many developers and the course correction to this approach.

3 Likes

I think this is a great call. I would suggest going further. The default enum is frozen and C enums and Apple standard library enums are annotated @NotFrozen.

2 Likes

I agree that having frozen as a default status would be nice: it means the safest, more controlled behaviour is default, and the less strict one is opt-in.
It also means that the change is less dramatic, as the old behaviour is stil the default one, and that the default behaviour is closer to the most commonly seen in other languages. That can make the language easier to learn for newcomers

1 Like