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