SE-0192 — Non-Exhaustive Enums

If we are not going to introduce a pattern matching for #unknown then I would like to put forward the idea of:
case default : This keeps the symmetry of case and default's position stays the same.

Well, the name #unknown is unfortunate because it implies that the case is only matched for unknown cases, which isn't true even of your unknown case, right? It's still a catch-all, essentially _, only it's a catch-all that we discourage users from using for statically-known cases. That's why I'm looking for a more generic spelling.

I think a less loaded syntax would make several of your examples much clearer.

1 Like

If unknown case _: were supported as a synonym, would that cause you to favor unknown default:?

If we're going with a warning, it is a possibility to reuse default for this task. To disable the warning, case _. default would still act the same as it does now, but just warn when there are unmatched, statically known cases.

Migration wouldn't be quite as straightforward, but since it's a warning, it wouldn't be urgently needed either. It would solve the clarity issue by not introducing an extra compound case label, and would be more discoverable than unknown.

My more general issue is that, once case _ can appear there, it implies any case pattern can appear there. I don't see a good way out of that, which is why I'm against unknown default syntax, rather than advocating for unknown case _ being allowed.

jrose
Jordan Rose

    January 31

karim:
If I’m re-compiling my code, I always want the compiler error to tell me I’m missing cases.

As mentioned in the proposal, it’s desirable to allow updating to a new version of a library without immediately having to change all your code. This is doubly important when you’re updating a library that another of your dependencies imports, since we’ve seen in practice that people do not like to modify their dependencies.

Is this a behaviour the language should embrace though (even if it seemed to appears pragmatic)?

Why is it desirable to allow uses to update to a version of a library that could bring you problem without having to change code? Especially in a language like Swift strongly opinionated about static compile time checks.

1 Like

When we think of building software, there essentially three states of the build:

  1. clean
  2. warning
  3. error

When you get an error - it's a full stop, fix everything until you can at least get all of the errors gone. So even if your 100k LOC codebase only uses a few instances of it, it's all stop. If it uses a lot more, it's even more problematic. Across potentially multiple teams, ugh... Especially in larger teams as it blocks adoption until things can get fixed and potentially staged out.

If you have warnings, we know there might be bugs or things that don't work properly, but it doesn't cause a full stop on work, especially for a team.

Being able to introduce, effectively "soft errors" can get your productivity up. They are still issues you should address, but you have your compiler outputting nice messages for you to go and do that as soon as you are able.

I'd really like to see more of this in the future. For instance, a required method on a subclass - the compiler can generate the stub for me, even if it crashes, and let me get to a more iterative workflow with compiled code instead of a sea of red (error messages) to get through to even start to see how broken things are.

1 Like

This is certainly a valid question. I have two concrete cases in mind:

  • It's useful if a dependency fixes a security issue or other serious bug but doesn't backport the fix, and so you need to pick up a newer version now.
  • It's useful if Apple releases a new SDK and you need to very quickly unbreak something on the latest OS. (Or you just want to try something out at WWDC.)

There are probably other situations as well, though these seem the most clear-cut to me. I personally think it's also just a nicer way to migrate to a newer version of a library. The difference between a warning and an error in Swift is generally that a warning doesn't stop you from building, but it doesn't mean it shouldn't stop you from shipping in most cases.

Just to clarify, I'm not saying we should necessarily use "unknown default:" specifically, I'm saying we should use some modifier on default, perhaps there is a better word than "unknown" that aligns with API evolution?

How about fallback default?

If you have warnings, we know there might be bugs or things that don’t work properly, but it doesn’t cause a full stop on work, especially for a team.

Isn’t this a recipe to go from P2 bugs to P1 or P0 ones when you introduce potentially buggy code that people can safely ignore the effects of? Not to start veering the discussion towards software craftsmanship not to be polemical, but it is an argument I do understand, but I find it almost as unconvincing as long term crunch as far as delivering (sustainable) productivity is concerned.

1 Like

jrose
Jordan Rose

    January 31

Panajev:
Why is it desirable to allow uses to update to a version of a library that could bring you problem without having to change code? Especially in a language like Swift strongly opinionated about static compile time checks.

This is certainly a valid question. I have two concrete cases in mind:

  • It’s useful if a dependency fixes a security issue or other serious bug but doesn’t backport the fix, and so you need to pick up a newer version now.
  • It’s useful if Apple releases a new SDK and you need to very quickly unbreak something on the latest OS. (Or you just want to try something out at WWDC.)
    There are probably other situations as well, though these seem the most clear-cut to me. I personally think it’s also just a nicer way to migrate to a newer version of a library. The difference between a warning and an error in Swift is generally that a warning doesn’t stop you from building, but it doesn’t mean it shouldn’t stop you from shipping in most cases.

Fair, but it may allow me to ship something subtly broken in worse aspect or introduce security issues... or even worse (see Intel microcode patch for spectre and meltdown).

Again, we are rushing to apply a hot fix we may not fully evaluate the risk of... also this is something that would call for a suppression of this rule locally/escape hatch rather than a general change for everyone. Emergency vs convenience.

Also, I can understand changes in a dynamic library more than changes una library you have the source code of.

2 Likes

Seems like you can just enable warnings-as-errors if this is your general philosophy.

Isn’t this a recipe to go from P2 bugs to P1 or P0 ones when you introduce potentially buggy code that people can safely ignore the effects of?

Here's the thing, there are multiple ways to address the errors, right? Often the goal is to pick up a library update in the least amount of time, the pragmatic thing to do is to do the minimum amount of work to shut the compiler up. Adding a case with fatalError("nyi") is the minimum work you can do.

Is it really worth developer time to do that, or would be it more beneficial to have the compiler generate when it can and provide you the warnings so you know to go fix it. I'd argue the warnings are better because they are reported and visible with no additional tooling required. To surface your "nyi" items, you need custom tools to emit either build errors or warnings.

No one is really arguing that you should ship with these warnings, sure, you can, but that gets into your craftsmanship argument.

As a developer, I want the compiler to help me get my work done and to ensure correctness as much as possible. However, I also need to be able to have in-progress code. If the compiler can generate the "in-progress" code for us and providing warnings for it, we get free tracking for it as well. That's a huge win-win to me.

... and I do in Objective-C and Swift :). This is about a decision in a language pointing at static safety (which has its costs as an approach too) that seems to go against it in order to deliver an emergency escape hatch.

Initially there was not even consensus this should be a warning for all those people running warnings as errors flows in production...

To elaborate on Jordan's first point: remember that the same sorts of things that are binary-compatibility issues with binary libraries are generally source-compatibility issues with source libraries. If we're going to maintain a healthy library ecosystem, it's important that libraries not be version-locked to all their dependencies, because their dependencies need to be able to be updated without immediately requiring all downstream libraries to be fixed. Otherwise, you might be artificially blocked from adopting important updates just because some library happens to take longer to update their code.

Let's make that more concrete. Suppose that your app uses a couple of different map-features libraries — Classico and Striits — that each in turn depends on Geography, a widely-used basic geography library. Classico comes out with a cool new update which provides location data for ancient Roman roads, but that update requires a newer version of Geography. The Striits maintainers have updated their code for that on their master branch, but they're also in the middle of a major rewrite of their database layer, so that branch is pretty unstable. The ideal here is that you can update Geography (and Classico) and that the old version of Striits will still compile and run. Of course, it's possible that the old version of Striits won't work with the new version of Geography, even if it does compile, but that's inevitable. Besides, part of the purpose of design features like source-resilience is to encourage people to write code that is more flexible about their upstream dependencies in the first place — like instead of trying to hard-code special behavior for every case of Geography.Sea, maybe there's some method you could be using instead.

some other options:

case default:
else:
case else:
else default:

For me, else: is less suggestive of exhaustivity-checking than default:, given its use in if {} else {}.

I can't help but think that this feature is best described as a ‘default’ for unmatched patterns, making plain default quite appropriate for exhaustivity checking (vs case _, which itself is an exhaustive pattern). If we do use a different modifier, it needs to point out the difference quite clearly.

Perhaps unmatched or unavailable would be suitable modifiers?

I think that this argument is not so closely connected to the conclusion as it feels at first glance. Consider the type error – the purpose of a type error is not to get the programmer to insert as Any to “handle" the diagnostic. The purpose of the type error is to present the programmer an opportunity to fix suspicious code. We choose diagnostics such that the probability of diagnostics in suspicious code is higher than in nonsuspicious code and so "ideally" in the process of resolving the diagnostic the suspiciousness of the code decreases.

The issue here is that “we don’t know” whether missing a case is suspicious or nonsuspicious, and so we don’t know whether or not to emit a diagnostic. For example, if we miss writing the “failure” case in a success/failure enum that would be suspicious. Alternatively if we have not localized a rare error into German that is non-suspicious.

Unifying these disparate cases under the banner of a warning is the worst of both worlds, both because it allows us to compile a program with missing error handling and because it produces unimportant diagnostics that non-German-speaking developers can’t resolve and will likely ignore. In fact the right behavior is an error in the former case, and no diagnostic at all in the latter. A warning is a poor attempt to split the difference.

What we require is a way to distinguish the situation where missing a case is suspicious from the situation where it is non-suspicious. In fact we have such a method today: the ‘default’ keyword distinguishes exhaustive switches from nonexhaustive ones.

We should continue to error if an exhaustive switch misses cases, and encourage non-exhaustive switches as a solution to that error. As you say, people don’t like editing their dependencies – so they definitely won’t if we let them off with a warning. And then they will accumulate a pile of “somebody else’s” warnings that will reduce the SnR of the diagnostic system as a whole.

Arguably, Swift has the wrong default here – nonexhaustive switches should be easier to write. But that’s not going to be fixed by demoting errors to warnings.

1 Like

Perhaps I'm missing something, but in this case it is by definition suspicious to miss a case because you've deliberately used whatever spelling that warns you when the rest of your switch is no longer exhaustive (unknown case/unknown default/whatever) rather than just using default:. This is what provides the distinction between situations that you're asking for. Under this proposal, all switches must still be exhaustive and I'm not sure anyone is arguing otherwise.

1 Like