Nullable switch

Brian Goetz brian.goetz at oracle.com
Fri Aug 7 14:48:13 UTC 2020


> Okay, so it would seem that we need two keywords (or other syntax) for 
> use in patterns; I will temporarily call them “anything-but-null” and 
> “anything-including-null”.

Not necessarily; the approach we've been driving towards has no (new) 
keywords, and no _explicit_ consideration of nullability. There's just 
type patterns, but their semantics take into account whether or not the 
type pattern "covers" the target type.  This is subtle, I grant, and I 
can see where people would get confused, but it is far more 
compositional and less ad-hoc.

Ignoring the epicyclical* distastefulness of the "any x" idea, I think 
the the syntax issues are a bit of a red herring -- the issue is 
structural.  Under Remi's proposal, there is simply _no_ way to write a 
switch where any number of cases covers "anything including null", 
because the switch will throw before you get there:

     switch (x) {
         case String s:
         case Object o:
     }

would throw on NPE (as switches do today) before any cases are 
considered, whether you say "var" or "any" or "Object."  This is, 
essentially, saying "switch is permanently polluted with null behavior, 
we're not going to do anything about it, and anyone who wants to treat 
nulls uniformly with other values just can't use switch (or has to 
duplicate code, etc.)  It also, as mentioned, provides pitfalls for 
several expected-to-be-common refactorings, because the "obvious" 
refactoring does not have the same semantics.

Instead, we are proposing to refine the handling of null in switch to 
work in line with _totality_ of patterns -- building on the use of 
totality in several other places.  If we have a total pattern (like 
`Object o`), we already use it for dead-code detection:

     switch (x) {
         case Object o: ...
         case P: // error, dead code, no matter what P is
     }

When we do pattern assignment, we use totality in flow analysis:

     Point p = ...
     ...
     Point(var x, var y) = p;  // OK, Point(...) is total on Point

     Object o = ...
     ...
     Point(var x, var y) = o;  // error, Point is not total on Object

Note that this definition of totality (so far) has a hole: nullity. We 
can check the static type of the operand and verify that the pattern 
covers all instances of that type, but without nullity in the type 
system, we can't statically exclude null.  So we propose to rectify 
that: a total pattern matches null too.  (You can consider `var x` to be 
an "anything" pattern, or consider it to be inference for the obvious 
type pattern; under this interpretation of total type patterns, the two 
get you to the same semantics.)  And this matches expected intuition (I 
claim) for what "case Box(Object o)" at the end of a switch on boxes 
should do -- but gets there entirely in terms of mechanical composition 
rules for patterns.

We can only use patterns in switch and instanceof, but both of these 
constructs currently have fail-fast behaviors with null.  So we are 
proposing to refine the null handling of switch (compatibly) so that we 
can work with, rather than against, totality.

*Celestial term for "bag nailed on the side"

> In particular, I am uncertain about this situation: suppose we have a 
> pattern FrogBox(Frog x) (that is, it is known that the argument to 
> FrogBox must be of type Frog), and Tadpole is a class that extends (or 
> implements) Frog; then consider
>
> FrogBox fb = …;
> switch (fb) {
> case FrogBox(Tadpole x) -> … ;
> case FrogBox(Frog x) -> … ;
> }
>
> (I think this is similar to previous examples, but I want the catchall 
> case to be Frog, not Object.)  Is the preceding switch total, or do I 
> need to say either

Yes.  Let's write out the declaration of the FrogBox pattern to see.

     class FrogBox {
         Frog f;

         deconstructor FrogBox(Frog f) {
             f = this.frog;
         }
     }

For

     case FrogBox(Frog x):

we do overload resolution to find the deconstruction pattern, and 
discover that its binding is of type Frog.  The nested pattern `Frog x` 
is total on `Frog`, and the deconstruction pattern FrogBox(Q) is total 
on FrogBox when Q is total on Frog, and therefore the compound pattern 
`FrogBox(Frog x)` is total on FrogBox (under the working proposal.)

Under Remi's proposal, it is not; it exlcudes FrogBox(null).  (Also, if 
this were a switch expression, you'd be told it is not exhaustive.)  
Further, under Remi's proposal, you can't easily refactor into a nested 
switch, because that would change the null behavior from "ignore" to 
"throw", unless you added an extra "if x == null".

If you wanted to be total under Remi's proposal, you'd have to change 
the last case to

     case FrogBox(any x):

But you'd still have no way to refactor to a nested switch without 
manually handling the null that pops out, and duplicating the code for 
FrogBox(null) and FrogBox(Frog).



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


More information about the amber-spec-experts mailing list