----- Mail original ----- > De: "Brian Goetz" <brian.go...@oracle.com> > À: "amber-spec-experts" <amber-spec-experts@openjdk.java.net> > Envoyé: Jeudi 10 Mai 2018 22:12:37 > Objet: Exhaustiveness in switch
> In the long (and indirect) thread on "Expression Switch Exception > Naming", we eventually meandered around to the question of when should > the compiler deem an expression switch to be exhaustive, and therefore > emit a catch-all throwing default. Let's step back from this for a bit > and remind ourselves why we care about this. > > Superficially, having to write a throwing default for a condition we > believe to be impossible is annoying: > > switch (trafficLight) { > case RED -> stop(); > case GREEN -> driveFast(); > case YELLOW -> driveFaster(); > default -> throw new ExasperationException("No, we haven't > added any new traffic light colors since the invention of the > automobile, so I have no idea what " + trafficLight + " is"); > } > > The annoyance here, though, is twofold: > - I have to write code for something which I think can't happen; > - That code is annoying to write. > > In the above, we "knew" another traffic light color was impossible, and > we listed them all -- and the compiler knew it. This is particularly > irritating. However, we often also see cases like this: > > void processVowel(letter) { > switch (letter) { > case A: ... > case E: ... > case I: ... > case O: ... > case U: ... > default: throw new IllegalStateException("Not a vowel: " + > letter); > } > > Here, the annoyance is slightly different, in that I could not > reasonably expect the compiler to know I'd covered all the vowels. In > fact, I think the explicit exception in this case is useful, in that it > documents an invariant known to the programmer but not captured in the > type system. But it is still annoying that I have to construct a format > string, construct an exception, and throw it; if there were easier ways > to do that, I might be less annoyed. Without diving into the bikeshed, > maybe this looks something like: > > default: throw IllegalStateException.format("Not a vowel: %s", > vowel); > > The details aren't relevant, but the point is: maybe a small-ish library > tweak would reduce the annoyance of writing such clauses. (This one > isn't so bad, but Dan excavated a bunch that were way worse.) But, > let's set this aside for a moment, and return back to the point of why > we want the compiler to provide a throwing default. > > I think most of the discussion has centered on the problem of a novel > value showing up at runtime. This is surely an issue, and must be dealt > with, but the central issue is: a default is never able to distinguish > between a runtime-novel value and a value we just forgot to include at > compile time. It doesn't matter whether this default throws (as the > implicit default in an expression switch) or does nothing (as the > implicit default in statement switches today does). > > We agreed that we should not require the user to provide a default when > they provide case clauses that cover the target type as of compile time > (true+false for boolean, all the members of a sealed type, etc.) This > is because the default you'd be forced to put in otherwise (for > expression switches) is actually harmful; if the type were later > modified to have more values, an explicit default would swallow them, > rather than yielding an error at recompilation time. So it is not only > annoying, but actually could cover up errors. > > We then went off on the wrong tangent, though, where we wondered whether > it was OK to implicitly assume enums were sealed, since some enums are > clearly intended to acquire new values. But the mistake was focusing on > the wrong aspect of sealed-ness (the statement of intent to not add more > values), rather than the compiler's ability to reason credibly about > known possible values. > > So, backing up, I think we should always treat a "complete" enum > expression switch specially -- don't require a default, and implicitly > add a throwing one, if all the cases are specified. This way, if the > assumption that you've covered all the cases is later broken via > separate compilation, on recompilation, you'll discover this early, > rather than at runtime. (You'll still get runtime protection either > way.) Regardless of whether we think the enum will be extended in the > future or not. There's no need for enums to declare themselves "sealed" > or "non-sealed" (and such a declaration would likely be incorrect > anyway, as it asks users to predict the future, which is error-prone.) > > Given this, I'm willing to use ICCE as a base type for the implicit > exception (though there should be more specific subtypes.) yes ! > > > Now, statement switches. It seems sad that we can't get the same kind > of compile-time assistance over statement switches than we do over > expression switches. We're somewhat locked in by compatibility here; > statement switches today get an implicit "default: nothing" clause if > they have no default, and we cannot (and don't want to) break this. So > the next best thing is if the user could say "I want to get the same > sort of compile-time verification of putative exhaustiveness for this > statement switch as I would for expression switches." This would > require some additional syntax (please, let's not bikeshed this until > everything else on this topic is nailed down; this is a target of > opportunity, not a problem to be solved Right Now.) > > Someone is likely to suggest that we should do the exhaustiveness thing > for all three of the four new forms (statement arrow, and expression > colon/arrow). Feel free to make this suggestion, but you're going to > get the "snitch" lecture :) > > > Another thing that we can do to make it easier to write throwing > defaults: lean on intrinsics. Recall that separately, we've got a story > to expose some compiler intrinsics for ldc() and invokedynamic(). > There's room to add other things to this, such as the equivalent of > __LINE__ and __FILE__ macros in C, or (relevant to this) information > about the the current point in the compilation (such as the cases > enumerated in the innermost switch.) So for example: > > default: throw SwitchException.format("Found %s, but expected one > of %s", > target, > Intrinsics.switchCases()); > > or even > > default: throw SwitchException.of(target, Intrinsics.switchCases()); > > where `Intrinsics.switchCases()` would evaluate to a string that > includes all the cases handled by the current switch (in our vowels > case, this would be "A, E, I, O, U"). Again, not something for Right > Now, but something that machinery that's in the pipeline can contribute > to making it simpler and more uniform to express catch-all defaults, and > thereby reduced the perceived annoyance. I think it's a little too magic, even for me. > > > Summary: > - For switches over any type where the compiler can enumerate the > possibilities (includes enums, some primitives, and sealed types), > always allow the user to leave off a default if they've specified all > the known cases. > - Use subtypes of ICCE in implicit throwing defaults. > - Consider library enhancements to common exceptions (and maybe > additional intrinsics) to simplify code that throws formatted exceptions. I do not think it's important to list existing case when reporting the error, thus i do not think Intrinsics.switchCases() worth its own weight. Rémi