Exhaustiveness in switch
Remi Forax
forax at univ-mlv.fr
Thu May 10 23:11:04 UTC 2018
----- Mail original -----
> De: "Brian Goetz" <brian.goetz at oracle.com>
> À: "amber-spec-experts" <amber-spec-experts at 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
More information about the amber-spec-experts
mailing list