> De: "Kevin Bourrillion" <kev...@google.com> > À: "Brian Goetz" <brian.go...@oracle.com> > Cc: "amber-spec-experts" <amber-spec-experts@openjdk.java.net> > Envoyé: Jeudi 10 Mai 2018 22:44:33 > Objet: Re: Exhaustiveness in switch
> * I think that for most occurrences of `default: throw...`, by far, the user > really doesn't benefit from being able to choose the exception type or the > message at all. A standardized choice of exception, and an autogenerated > message (that automatically includes the switch target, which users usually > don't bother to do themselves!), may be strictly better. +1, as one of my student put it, it's important to always go banana predictably. Rémi > On Thu, May 10, 2018 at 1:12 PM, Brian Goetz < [ > mailto:brian.go...@oracle.com | > brian.go...@oracle.com ] > wrote: >> 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.) >> 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. >> 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. > -- > Kevin Bourrillion | Java Librarian | Google, Inc. | [ > mailto:kev...@google.com | > kev...@google.com ]