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:

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

Maurizio:
> 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 uncomfortable.

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.





Reply via email to