Exhaustiveness in switch

Kevin Bourrillion kevinb at google.com
Thu May 10 20:44:33 UTC 2018

This is just a quick initial response and that it will veer off-topic:

* Users of Guava's Preconditions love the support for "%s"-formatting.  But
on the other hand, in code where you *are definitely* about to throw
something, why wouldn't I just use string concatenation? That is generally
easier to read because you don't have to mentally correlate specifiers to
faraway arguments. (Okay: one reason might be if your message is long
enough that you'd like to use a multi-line raw string literal for it, where
concatenation is BadBadBad, but I can't say how common this is or isn't at
the moment.)

* If you do add those methods, though, you don't want to just call
String.format() inside -- or if you do, you at least want to catch
InvalidFormatException and produce a fallback message in that case; don't
let that except supplant the one the user is really trying to create
(especially if that is unchecked).

* 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. Can
we consider providing them a shortcut (strawman: just `default: throw;`!)?

* For exhaustive switches (so far: enums), the ideal we have been trying to
shoot for is that the user makes an informed, intelligent choice about
whether they want the compile error or the runtime error when a new one is
added. So far, with what we've implemented at Google, it seems that we're
falling fall short of that ideal. We have been strongly considering the
idea that you *shouldn't* be allowed to simply omit a default at all,
because what that *means* is far too opaque. It would be better if the user
had to do something explicit in either case. Could it be that the idea in
the previous bullet actually provides this? We would go back to always
requiring an explicit `default` for an e-switch, but you still get to make
an explicit choice which behavior you're asking for.

* It would feel strange to even bother applying this exhaustiveness goo to
*byte* switches. If we ever had ranges.... of course then, any type of
switch could join the party. (I don't know whether ranges are a thing we're
considering or not and I'm not pushing that we do.)

On Thu, May 10, 2018 at 1:12 PM, Brian Goetz <brian.goetz at 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. | kevinb at google.com
-------------- next part --------------
An HTML attachment was scrubbed...
URL: <http://mail.openjdk.java.net/pipermail/amber-spec-experts/attachments/20180510/dfc622bc/attachment-0001.html>

More information about the amber-spec-experts mailing list