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.


More information about the amber-spec-experts mailing list