Reiterate total pattern accepting null in switch

Brian Goetz brian.goetz at oracle.com
Mon Sep 6 13:46:19 UTC 2021



On 9/6/2021 5:12 AM, Tagir Valeev wrote:
> Hello!
>
> Now, as we develop support in IntelliJ, we have a little bit of
> experience with patterns in switches. So far, the thing I dislike the
> most is that the total pattern matches null in the switch. I shared my
> concerns before and now they are basically the same, probably even
> stronger. Note that I'm not against total pattern matching null in
> general (e.g., as a nested pattern in deconstruction). But I still
> think that matching it at the top level of the switch is a mistake.
> Mentally, the total pattern is close to the default case.

I think there are several issues here; let's try to tease them apart.

The main problem, as I see it, is not one of whether we picked the right 
default, or whether that default is unfamiliar (though these are both 
valid things to discuss), but that we are putting the user to a 
non-orthogonal choice.  They can say:

     case Object o:

and get binding and null-matching, or

     default:

and get neither binding nor null-matching.

In some way, you are saying that there is a significant contingency 
where users want binding but not null-matching, and we're forcing users 
to take a package deal.  As a thought experiment, how differently would 
you feel if we had both nullable and non-nullable type patterns:

     case String! s:
     case String? s:

If we had the ability to refine the match in this way, then the choice 
of binding and null handling would be orthogonal:

     case Object! o:   // binding, no null
     case Object? o:   // binding, null
     default:          // no binding, no null
     null, default:    // no binding, null

So the first thought experiment I am asking you to do is whether, in 
this world, you would feel significantly differently.

> Also,
> adding a guard means that we do not receive null at this branch
> anymore which is also confusing.

Good point, I'll think on this a bit.

To reiterate the motivation, the thing we're going for here is the 
consistency that a switch of nested patterns and a nested switch are the 
same:

     case Box(Foo f):
     case Box(Object o):

is the same as

     case Box b:
         switch (b.get()) {
             case Foo f:
             case Object o:
         }
     }

If we treat the null at the top level, we get a different kind of 
asymmetry.  What we're banking on (which could be wrong) is that its 
better to rip off the band-aid rather than cater to legacy assumptions 
about switch.

I think what you are saying here is that switch is so weird that it is 
just a matter of pick your asymmetry, and the argument for moving it to 
the top level is that this is weirdness people are already used to.  
This may be true, though I worry that it is just that people are not 
*yet* used to nested switches, but they'll be annoyed when they get bit 
by refactoring issues.

> E.g., `case Object obj` accepts null
> but `case Object obj && obj != null` is meaningless as `obj != null`
> is always true. Well, making the pattern non-total immediately
> requires adding a default case, so you cannot just add a guard and do
> nothing else. Still, it's mentally confusing:
>
> switch(x) {
>    ... other cases
>    case Object obj -> ... // null goes here
> }
>
> switch(x) {
>    ... other cases
>    case Object obj && obj != null -> ... // exclude null
>    default -> // add default, as compiler requests. Now only 'null' is
> the remainder. But why it doesn't go here?
> }

OK, but write those cases out nested one level in Box(); are you not 
equally unhappy there?




More information about the amber-spec-experts mailing list