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?


Reply via email to