I just want to add that there is another possible semantics than having case 
null + total pattern as null acceptable pattern. 

We have based the semantics of pattern matching to the semantics of the cascade 
of if instanceof with an else at the end. 
There is another one, method calls [1], a switch like this 
switch(o) { 
case String s: 
case Object o: 
} 
can be seen as equivalent to the methods 
void m(String s) { ... 
void m(Object o) { ... 
with the switch being equivalent to a call to the method m. 

This semantics is equivalent to the cascade of instanceof apart in one 
scenario, when the value is null. 
A call to m(null) invokes m(String s), not m(Object). 

You may think that the method call semantics is not a good one because it can 
answer that no method are applicable or no method are more specific than the 
other, 
but those cases are not possible if the switch is total ... apart with null. 

If the value is null and we have a switch like this 
switch(o) { 
case String s: 
case Integer i: 
case Object o: 
} 
if we use the method call semantics, either "String s" or "Integer i" can be 
called, and given that none is more specific than the other. 

We can deviate from the original method call semantics by saying, let's take 
the first match, but in that case, it's just a different semantics, not an 
existing Java semantics. 

I don't like this semantics, because it will be surprising for anyone that null 
will be captured by the first case. I prefer null to be trapped by the total 
pattern at the end. 

Sadly, it means that there is no *obvious* semantics for null and that whatever 
we choose, it will not what some people expect. 

Rémi 

[1] https://docs.oracle.com/javase/specs/jls/se16/html/jls-15.html#jls-15.12 

> De: "Brian Goetz" <brian.go...@oracle.com>
> À: "amber-spec-experts" <amber-spec-experts@openjdk.java.net>
> Envoyé: Vendredi 23 Avril 2021 17:38:42
> Objet: Re: Switch labels (null again), some tweaking

> 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