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?