Switch labels (null again), some tweaking

Brian Goetz brian.goetz at oracle.com
Wed Apr 28 17:27:16 UTC 2021


I think part of the problem is that we're using the word "total" in 
different ways.

A pattern P may be total on type T with remainder R.  For example, the 
pattern `Soup s` is total on `Soup` with no remainder.

A _set_ of patterns may be total on T with remainder R as well. (The 
only way a set of patterns is total is either (a) one of the patterns in 
the set is already total on T, OR (b) sealing comes into play.)  Maybe 
this should be called "exhaustive" to separate from pattern totality.

Switch exhaustiveness derives from set-totality.

Instanceof prohibits patterns that are total without remainder, for two 
reasons: (1) its silly to ask a question which constant-folds to `true`, 
and (b) the disagreement between traditional `instanceof Object` and 
`instanceof <total pattern>` at null would likely be a source of bugs.  
(This was the cost of reusing instanceof rather than creating a new 
"matches" operator.)

> Foo x = ...
> if (x instanceof Bar)
>
> The instanceof will not be considered total, and therefore be accepted 
> by the compiler (sorry to repeat the same question - I want to make 
> sure I understand how totality works with sealed hierarchies).
>

If the RHS of an `instanceof` is a type (not a type pattern), then this 
has traditional `instanceof` behavior.  If Bar <: Foo, then this is in 
effect a null check.

If the RHS is a _pattern_, then the pattern must not be total without 
remainder.  If Bar <: Foo, `Bar b` is total on Foo, so the compiler says 
"dumb question, ask a different one."

If the RHS is a non-total pattern, or a total pattern with remainder, 
then there's a real question being asked.  So in your Lunch-permits-Soup 
example, you could say

     if (lunch instanceof Soup s)

and this matches _on non-null lunch_.  Just like the switch. The only 
difference is switch will throw on unmatched nulls, whereas instanceof 
says "no, not an instance", but that's got nothing to do with patterns, 
it's about conditional constructs.

> If that's the case, I find that a bit odd - because enums kind of have 
> the same issue (but we have opted to trust that a switch on an enum is 
> total if all the constants known at compile time are covered) - and, 
> to my eyes, if you squint, a sealed hierarchy is like an enum for 
> types (e.g. sum type).
>

OK, let's talk about enums. Suppose I have:

     enum Lunch { Soup }

and I do

     switch (lunch) {
         case Soup -> ...
     }

What happens on null?  It throws, and it always has.  The behavior for 
the sealed analogue is the same; the `Soup s` pattern matches the 
non-null lunches, and if null is left unhandled elsewhere in the switch, 
the switch throws.  No asymmetry.

> Anyway, backing up - this below:
>
> ```
>
> switch (lunch) {
>         case Box(Soup s):
>               System.err.println("Box of soup");
>               break;
>
>         case Bag(Soup s):
>              System.err.println("Bag of soup");
>              break;
>
>         /* implicit */
>         case Box(null), Bag(null): throw new NPE();
>     }
> ```
>
> is good code, which says what it means. I think the challenge will be 
> to present error messages (e.g. if the user forgot to add case 
> Box(null)) in a way that makes it clear to the user as to what's 
> missing; and maybe that will be enough.
>

The challenge here is that we don't want to force the user to handle the 
"silly" cases, such as:

     Boolean bool = ...
     switch (bool) {
         case true -> ...
         case false -> ...
         case null -> ...  // who would be happy about having to write 
this case?
     }

and the similarly lifted:

     Box<Boolean> box = ...
     switch (box) {
         case Box(true) -> ...
         case Box(false) -> ...
         case Box(null) -> ...  // who would be happy about having to 
write this case?
         case Box(novel) -> ... // or this case?
         case null ->           // or this case?
     }

So we define the "remainder" as the values that "fall into the cracks 
between the patterns."  Users can write patterns for these, and they'll 
match, but if not, the compiler inserts code to catch these and throw 
something.

The benefit is twofold: not only does the user not have to write the 
stupid cases (imagine if Box had ten slots, would we want to write the 
2^10 partial null cases?), but because we throw on the remainder, DA can 
treat the switch as covering all boxes, and be assured there are no leaks.

-------------- next part --------------
An HTML attachment was scrubbed...
URL: <https://mail.openjdk.java.net/pipermail/amber-spec-experts/attachments/20210428/e081531f/attachment-0001.htm>


More information about the amber-spec-experts mailing list