Switch labels (null again), some tweaking

Brian Goetz brian.goetz at oracle.com
Fri Apr 23 15:38:42 UTC 2021

Gavin has a spec that captures most of what we've talked about here, 
which should be posted soon.  But I want to revisit one thing, because I 
think we may have swung a little too eagerly at a bad pitch.

There is a distinction between the _semantics of a given pattern_ and 
_what a given construct might do with that pattern_.  The construct 
(instanceof, switch) gets first crack at having an opinion, and then may 
choose to evaluate whether the pattern matches something.  The place 
where we have been most tempted to use this flexibility is with "null", 
because null ruins everything.

We made a decision to lump pattern matching in with `instanceof` because 
it seemed silly to have two almost identical but subtly different 
constructs for "dynamic type test" and "pattern match" (given that you 
can do dynamic type tests with patterns.)  We knew that this would have 
some uncomfortable consequences, and what we have tentatively decided to 
do is outlaw total patterns in instanceof, so that users are not 
confronted with the subtle difference between `x instanceof Object` and 
`x instanceof Object o`.  This may not be a totally satisfying answer, 
and we left some room to adjust this, but its where we are.

The fact is that the natural interpretation of a total type pattern is 
that it matches null.  People don't like this, partially because it goes 
against the intuition that's been built up by instanceof and switch.  
(If we have to, I'm willing to lay out the argument again, but after 
having been through it umpteen times, I don't see any of the 
alternatives as being better.)

So, given that a total type pattern matches null, but legacy switches 
reject null...

The treatment of the `null` label solves a few problems:

  - It lets people who want to treat null specially in switch do so, 
without having to do so outside the switch.
  - It lets us combine null handling with other things (case null, 
default:), and plays nicely when those other things have bindings (case 
null, String s:).
  - It provides a visual cue to the reader that this switch is nullable.

It is this last one that I think we may have over-rotated on. In the 
treatment we've been discussing, we said:

  - switch always throws on null, unless there's a null label

Now, this is clearly appealing from a "how do I know if a switch throws 
NPE or not" perspective, so its understandable why this seemed a clever 
hack.  But reading the responses again:

 > I support making case null the only null-friendly pattern.

 > there's no subtle type dependency analysis which determines the fate 
of null

it seems that people wanted to read way more into this than there was; 
they wanted this to be a statement about patterns, not about switch.  I 
think this is yet another example of the "I hate null so much I'm 
willing to take anything to make it (appear to) go away" biases we all 
have."  Only Remi seemed to have recognized this for the blatant trick 
it is -- we're hiding the null-accepting behavior of total patterns 
until people encounter them with nested patterns, where it will be less 

To be clear: this is *not* making `null` the only null-friendly 
pattern.  But these responses make me worry that, if the experts can't 
tell the difference, then no user will be able to tell the difference, 
and that this is just kicking the confusion down the road.  It might be 
better to rip the band-aid off, and admit how patterns work.

Here's an example of the kind of mistake that this treatment 
encourages.  If we have:

     switch (x) {
         case Foo(String a):  A
         case Foo(Integer a): B
         case Foo(Object a):  C

and we want to refactor to

switch (x) {
         case Foo(var a):
             switch(a) {
                 case String a:  A
                 case Integer a: B
                 case Object a:  C

we've made a mistake.  The first switch does the right thing; the second 
will NPE on Foo(null).  And by insulating people from the real behavior 
of type patterns, it will be even more surprising when this happens.

Now, let's look back at the alternative, where we keep the flexibility 
of the null label, but treat patterns as meaning what they mean, and 
letting switch decide to throw based on whether there is a nullable 
pattern or not.  So a switch with a total type pattern -- that is, `var 
x` or `Object x` -- will accept null, and thread it into the total case 
(which also must be the last case.)

Who is this going to burn, that is not going to be burned by the 
existing switch behavior anyway?  I think very, very few people.  To get 
burned, a lot of things have to come together.  People are used to 
saying `default`; those that continue to are not going to get burned.  
People are generally in agreement that `var x` should be total; people 
who use that are not going to get burned.  Switches today NPE eagerly on 
null, so having a null flow into code that doesn't expect it will result 
in ... the same NPE.

And, people who want to be explicit can say:

     case null, Object o:

and it will work -- and maybe even IntelliJ will hint them "hey, did you 
know this null is redundant?"  And then learning will happen!

So, I think the "a switch only accepts null if the letters n-u-l-l are 
present", while a comforting move in the short term, buys us relatively 
little, and dulls our pain receptors which in turn makes it take way 
longer to learn how patterns really work.  I think we should go back to:

  - A switch accepts null if (a) one of the case labels is `null` or (b) 
the switch has a total pattern (which must always be the last case.)

On 3/12/2021 9:12 AM, Brian Goetz wrote:
> The JEP has some examples of how the `null` case label can combine 
> with others.  But I would like to propose a more general way to 
> describe what's going on.  This doesn't change the proposed language 
> (much), as much as describing/specifying it in a more general way.
> We have the following kinds of switch labels:
>     case <constant>
>     case null
>     case <pattern>
>     default
> The question is, which can be combined with each other into a single 
> case, such as:
>     case 3, null, 5:
> This question is related to, which can fall into each other:
>     case 3:
>     case null:
>     case 5:
> We can say that certain labels are compatible with certain others, and 
> ones that are compatible can be combined / are candidates for 
> fallthrough, by defining a compatibility predicate:
>  - All <constant> case labels are compatible with each other;
>  - The `null` label is compatible with <constant> labels;
>  - The `null` label is compatible with `default`;
>  - The `null` label is compatible with (explicit) type patterns:
> (There is already a check that each label is applicable to the type of 
> the target.)
> Then we say: you can combine N case labels as long as they are all 
> compatible with each other.  Combination includes both comma-separated 
> lists in one case, as well as when one case is _reachable_ from 
> another (fall through.)  And the two can be combined:
>     case 3, 4: // fall through
>     case null:
> So the following are allowed:
>     case 1, 2, 3, null:
>     case null, 1, 2, 3:
>     case null, Object o:
>     case Object o, null:
>     case null, default:    // special syntax rule for combining default
> The following special rules apply:
>  - `default` can be used as a case label when combined with compatible 
> case labels (see last example above);
>  - When `null` combines with a type pattern, the binding variable of 
> the type pattern can bind null.
> The semantics outlined in Gavin's JEP are unchanged; this is just a 
> new and less fussy way to describe the behavior of the null label / 
> specify the interaction with fallthrough.

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

More information about the amber-spec-experts mailing list