Re: [External] : Re: Treatment of total patterns (was: Reviewing feedback on patterns in switch)

2022-01-28 Thread Brian Goetz
You can say you only change the semantics of switch not the semantics 
of pattern matching, but the idea that you can separate the two is 
confusing.


From a mathematical point of view, it is quite clear.  We define a `x 
matches P` relation.  In this relation, `Object o` matches all values of 
x, including null.


Then, we define the semantics of `instanceof` and `switch`.  For 
example, `x instanceof P` means: "if x is null, then false, otherwise 
evaluates to `x matches P`."  The construct gets to decide when to 
evaluate the pattern.


This is just like how we separate the inference machinery from how 
inference is used (differently) to produce a result for diamond or var.


What you're saying, I think, is that most users don't separate the 
layers like this; their understanding of pattern matching is conflated 
with how pattern contexts like switch/instanceof work. And that is 
surely true.  But having a clear definition of how pattern matching 
works, and a clear definition of how switch/instanceof use pattern 
matching, allows the users who *do* want to understand, to do so more 
easily, because we've separated the concepts.


PS: the feedback about the fact that it's not clear if a switch allows 
null or not can also be seen as a symptom of the fact that the notion 
of total pattern is not obvious for everybody (and having no syntax 
hint does not help).


I think this is the real issue; leaning on totality is more sound and 
less ad-hoc, but harder to learn.  You'd like to make that easier to 
learn by introducing more syntax; I'm saying that this is (a) more 
complicated in the long run, and (b) way over-rotating towards treatment 
of null.


Re: [External] : Re: Treatment of total patterns (was: Reviewing feedback on patterns in switch)

2022-01-28 Thread forax
> From: "Brian Goetz" 
> To: "Remi Forax" 
> Cc: "Tagir Valeev" , "amber-spec-experts"
> 
> Sent: Thursday, January 27, 2022 4:41:27 PM
> Subject: Re: [External] : Re: Treatment of total patterns (was: Reviewing
> feedback on patterns in switch)

>> In that case, i prefer the current semantics because it's the same if a 
>> pattern
>> is a top-level or not.

> I wish people could keep these things straight. We’re not talking about 
> changing
> the semantics of how pattern matching works, which patterns match what, what
> nesting means, etc. We’re simply talking about the *boundary* between a
> specific pattern-accepting construct, which has pre-existing value filtering
> opinions, and the patterns it accepts.

> The current (preview) boundary says:

> - If a switch has a `case null`, or a total pattern, a null value matches 
> that,
> otherwise we throw NPE on null, and for non-null, it is matched to the 
> patterns
> in the case labels.

> The adjusted boundary says:

> - If a switch has a `case null`, a null value matches that, otherwise we throw
> NPE on null, and for non-null, it is matched to the patterns in the case 
> label.

> So this adjusts *which* patterns the switch lets see null values. Previously, 
> it
> was “none”; in the current preview, it is “case null and total patterns”, and
> the adjustment proposed is “case null”. The latter is a tradeoff to avoid
> confusing the users, who currently believe switch always throws on null, by
> saying “switch accepts null if it says case null.”

> We currently have a similar problem with `intsnaceof`, where we disallow total
> patterns on the RHS of instanceof. We would adjust in the same way: instanceof
> always says false on nulls, and tests against the RHS on non-null.

> Nothing to do with the semantics of pattern matching. Total patterns are still
> total.

You can say you only change the semantics of switch not the semantics of 
pattern matching, but the idea that you can separate the two is confusing. 

For me, the semantics of pattern matching change because currently a total 
pattern always match null, whatever its position, as top-level or inside a 
record pattern (for example), 
with the semantics you propose a top-level pattern will not match null anymore 
but will match null if nested. 

So yes, i suppose you can say that the semantics of a total pattern is not 
changed because whatever the position it *can* match null, but 'm not sure this 
way of thinking helps. 

To make thing super clear, with the current semantics, "case Object o" always 
match null, with your proposal, the answer is it depends if it is nested or 
not. 
That's why i prefer the current semantics. 

regards, 
Rémi 

PS: the feedback about the fact that it's not clear if a switch allows null or 
not can also be seen as a symptom of the fact that the notion of total pattern 
is not obvious for everybody (and having no syntax hint does not help). 


Re: [External] : Re: Treatment of total patterns (was: Reviewing feedback on patterns in switch)

2022-01-27 Thread Brian Goetz


In that case, i prefer the current semantics because it's the same if a pattern 
is a top-level or not.


I wish people could keep these things straight.  We’re not talking about 
changing the semantics of how pattern matching works, which patterns match 
what, what nesting means, etc.  We’re simply talking about the *boundary* 
between a specific pattern-accepting construct, which has pre-existing value 
filtering opinions, and the patterns it accepts.

The current (preview) boundary says:

 - If a switch has a `case null`, or a total pattern, a null value matches 
that, otherwise we throw NPE on null, and for non-null, it is matched to the 
patterns in the case labels.

The adjusted boundary says:

 - If a switch has a `case null`, a null value matches that, otherwise we throw 
NPE on null, and for non-null, it is matched to the patterns in the case label.

So this adjusts *which* patterns the switch lets see null values.  Previously, 
it was “none”; in the current preview, it is “case null and total patterns”, 
and the adjustment proposed is “case null”.  The latter is a tradeoff to avoid 
confusing the users, who currently believe switch always throws on null, by 
saying “switch accepts null if it says case null.”

We currently have a similar problem with `intsnaceof`, where we disallow total 
patterns on the RHS of instanceof.  We would adjust in the same way: instanceof 
always says false on nulls, and tests against the RHS on non-null.

Nothing to do with the semantics of pattern matching.  Total patterns are still 
total.


Re: [External] : Re: Treatment of total patterns (was: Reviewing feedback on patterns in switch)

2022-01-27 Thread forax
> From: "Brian Goetz" 
> To: "Remi Forax" 
> Cc: "Tagir Valeev" , "amber-spec-experts"
> 
> Sent: Wednesday, January 26, 2022 3:08:39 PM
> Subject: Re: [External] : Re: Treatment of total patterns (was: Reviewing
> feedback on patterns in switch)

> I don’t think its helpful to try and reopen these old and settled issues. I 
> get
> that you think null should have a larger syntactic presence in the language,
> and you’ve made those points plenty of times, but we’re not reopening whether
> `Object o` is total, or whether `var` is more than type inference. We’re
> focused here on the interaction between switch and patterns, precisely because
> switch comes to the table with pre-existing null hostilities. We are not going
> to distort the semantics of pattern matching just so we can extrapolate from
> how C switch worked; we’ve been over this too many times.
In that case, i prefer the current semantics because it's the same if a pattern 
is a top-level or not. 

Rémi 

>> On Jan 26, 2022, at 8:45 AM, [ mailto:fo...@univ-mlv.fr |
>> fo...@univ-mlv.fr ] wrote:

>>> From: "Brian Goetz" < [ mailto:brian.go...@oracle.com | 
>>> brian.go...@oracle.com ]
>>> >
>>> To: "Remi Forax" < [ mailto:fo...@univ-mlv.fr | fo...@univ-mlv.fr ] >
>>> Cc: "Tagir Valeev" < [ mailto:amae...@gmail.com | amae...@gmail.com ] >,
>>> "amber-spec-experts" < [ mailto:amber-spec-experts@openjdk.java.net |
>>> amber-spec-experts@openjdk.java.net ] >
>>> Sent: Wednesday, January 26, 2022 1:47:38 PM
>>> Subject: Re: [External] : Re: Treatment of total patterns (was: Reviewing
>>> feedback on patterns in switch)

>>> Heh, you are incrementally rediscovering exactly why we chose the original
>>> “total is total” rule; of all the possible treatments, it is the most 
>>> logically
>>> consistent. Welcome.

>>> In this case, however, switches must be total. So here, either D is total
>>> (perhaps with remainder), or B/C/D cover whatever the content of Box is, or 
>>> it
>>> doesn’’t compile. If there is remainder (which is likely to be null, but 
>>> which
>>> won’t happen with a type pattern, only when D is more complicated), and no
>>> later case handles Box(null), then the switch will NPE. We don’t know if
>>> Box(null) is matched by any of these cases, but we *do* know that we will 
>>> not
>>> arrive at the statement after the switch if the target was Box(null).

>> It's true that if you can observe the different side effects when the code is
>> run, and from that you may have an idea if case Box(D d) matches or not (and
>> prey that there is not a catch() in the middle),
>> but the bar is very low if you say that to understand a code you have to run 
>> it.

>>> The proposed change to top-level null hostility doesn’t affect that.

>> yes, that my point, having to run a code to understand it is a clue that the
>> semantics you propose or the Java 18 one are both equally bad.

>> Again, the C# semantics does not have such problem, if we suppose that the 
>> code
>> compiles then with the code below, d can not be null

>> switch(box) {
>> case Box(B b) -> { }
>> case Box(C c) -> { }
>> case Box(D d) -> { } // does not accept null
>> }

>> while with this code, d can be null

>> switch(box) {
>> case Box(B b) -> { }
>> case Box(C c) -> { }
>> case Box(var d) -> { } // accept null
>> }

>> Rémi

 On Jan 26, 2022, at 2:53 AM, Remi Forax < [ mailto:fo...@univ-mlv.fr |
 fo...@univ-mlv.fr ] > wrote:

 We should go a step further, this also means that with

 switch(box) {
 case Box(B b) -> {}
 case Box(C c) -> {}
 case Box(D d) -> {}
 }

 we have no idea if the switch will accept Box(null) or not.

 So the idea that a type behave differently if nested inside a pattern or 
 not is
 not a good one.


Re: [External] : Re: Treatment of total patterns (was: Reviewing feedback on patterns in switch)

2022-01-26 Thread Brian Goetz
I don’t think its helpful to try and reopen these old and settled issues.  I 
get that you think null should have a larger syntactic presence in the 
language, and you’ve made those points plenty of times, but we’re not reopening 
whether `Object o` is total, or whether `var` is more than type inference.  
We’re focused here on the interaction between switch and patterns, precisely 
because switch comes to the table with pre-existing null hostilities.  We are 
not going to distort the semantics of pattern matching just so we can 
extrapolate from how C switch worked; we’ve been over this too many times.



On Jan 26, 2022, at 8:45 AM, fo...@univ-mlv.fr wrote:




From: "Brian Goetz" mailto:brian.go...@oracle.com>>
To: "Remi Forax" mailto:fo...@univ-mlv.fr>>
Cc: "Tagir Valeev" mailto:amae...@gmail.com>>, 
"amber-spec-experts" 
mailto:amber-spec-experts@openjdk.java.net>>
Sent: Wednesday, January 26, 2022 1:47:38 PM
Subject: Re: [External] : Re: Treatment of total patterns (was: Reviewing 
feedback on patterns in switch)
Heh, you are incrementally rediscovering exactly why we chose the original 
“total is total” rule; of all the possible treatments, it is the most logically 
consistent.  Welcome.

In this case, however, switches must be total.  So here, either D is total 
(perhaps with remainder), or B/C/D cover whatever the content of Box is, or it 
doesn’’t compile.  If there is remainder (which is likely to be null, but which 
won’t happen with a type pattern, only when D is more complicated), and no 
later case handles Box(null), then the switch will NPE.  We don’t know if 
Box(null) is matched by any of these cases, but we *do* know that we will not 
arrive at the statement after the switch if the target was Box(null).

It's true that if you can observe the different side effects when the code is 
run, and from that you may have an idea if case Box(D d) matches or not (and 
prey that there is not a catch() in the middle),
but the bar is very low if you say that to understand a code you have to run it.


The proposed change to top-level null hostility doesn’t affect that.

yes, that my point, having to run a code to understand it is a clue that the 
semantics you propose or the Java 18 one are both equally bad.

Again, the C# semantics does not have such problem, if we suppose that the code 
compiles then with the code below, d can not be null

switch(box) {
  case Box(B b) -> { }
  case Box(C c) -> { }
  case Box(D d) -> { }  // does not accept null
}

while with this code, d can be null

switch(box) {
  case Box(B b) -> { }
  case Box(C c) -> { }
  case Box(var d) -> { }  // accept null
}


Rémi


On Jan 26, 2022, at 2:53 AM, Remi Forax 
mailto:fo...@univ-mlv.fr>> wrote:

We should go a step further, this also means that with

switch(box) {
  case Box(B b) -> {}
  case Box(C c) -> {}
  case Box(D d) -> {}
  }

we have no idea if the switch will accept Box(null) or not.

So the idea that a type behave differently if nested inside a pattern or not is 
not a good one.



Re: [External] : Re: Treatment of total patterns (was: Reviewing feedback on patterns in switch)

2022-01-26 Thread forax
> From: "Brian Goetz" 
> To: "Remi Forax" 
> Cc: "Tagir Valeev" , "amber-spec-experts"
> 
> Sent: Wednesday, January 26, 2022 1:47:38 PM
> Subject: Re: [External] : Re: Treatment of total patterns (was: Reviewing
> feedback on patterns in switch)

> Heh, you are incrementally rediscovering exactly why we chose the original
> “total is total” rule; of all the possible treatments, it is the most 
> logically
> consistent. Welcome.

> In this case, however, switches must be total. So here, either D is total
> (perhaps with remainder), or B/C/D cover whatever the content of Box is, or it
> doesn’’t compile. If there is remainder (which is likely to be null, but which
> won’t happen with a type pattern, only when D is more complicated), and no
> later case handles Box(null), then the switch will NPE. We don’t know if
> Box(null) is matched by any of these cases, but we *do* know that we will not
> arrive at the statement after the switch if the target was Box(null).

It's true that if you can observe the different side effects when the code is 
run, and from that you may have an idea if case Box(D d) matches or not (and 
prey that there is not a catch() in the middle), 
but the bar is very low if you say that to understand a code you have to run 
it. 

> The proposed change to top-level null hostility doesn’t affect that.

yes, that my point, having to run a code to understand it is a clue that the 
semantics you propose or the Java 18 one are both equally bad. 

Again, the C# semantics does not have such problem, if we suppose that the code 
compiles then with the code below, d can not be null 

switch(box) { 
case Box(B b) -> { } 
case Box(C c) -> { } 
case Box(D d) -> { } // does not accept null 
} 

while with this code, d can be null 

switch(box) { 
case Box(B b) -> { } 
case Box(C c) -> { } 
case Box(var d) -> { } // accept null 
} 

Rémi 

>> On Jan 26, 2022, at 2:53 AM, Remi Forax < [ mailto:fo...@univ-mlv.fr |
>> fo...@univ-mlv.fr ] > wrote:

>> We should go a step further, this also means that with

>> switch(box) {
>> case Box(B b) -> {}
>> case Box(C c) -> {}
>> case Box(D d) -> {}
>> }

>> we have no idea if the switch will accept Box(null) or not.

>> So the idea that a type behave differently if nested inside a pattern or not 
>> is
>> not a good one.


Re: [External] : Re: Treatment of total patterns (was: Reviewing feedback on patterns in switch)

2022-01-26 Thread Brian Goetz
Heh, you are incrementally rediscovering exactly why we chose the original 
“total is total” rule; of all the possible treatments, it is the most logically 
consistent.  Welcome.

In this case, however, switches must be total.  So here, either D is total 
(perhaps with remainder), or B/C/D cover whatever the content of Box is, or it 
doesn’’t compile.  If there is remainder (which is likely to be null, but which 
won’t happen with a type pattern, only when D is more complicated), and no 
later case handles Box(null), then the switch will NPE.  We don’t know if 
Box(null) is matched by any of these cases, but we *do* know that we will not 
arrive at the statement after the switch if the target was Box(null).  The 
proposed change to top-level null hostility doesn’t affect that.

So I’m not sure what your objection is?

On Jan 26, 2022, at 2:53 AM, Remi Forax 
mailto:fo...@univ-mlv.fr>> wrote:

We should go a step further, this also means that with

switch(box) {
  case Box(B b) -> {}
  case Box(C c) -> {}
  case Box(D d) -> {}
  }

we have no idea if the switch will accept Box(null) or not.

So the idea that a type behave differently if nested inside a pattern or not is 
not a good one.



Re: [External] : Re: Treatment of total patterns (was: Reviewing feedback on patterns in switch)

2022-01-26 Thread Brian Goetz
> I strongly support this change.
> 
> In my experience, it's much more important to have automatic
> refactorings between switch and chains of 'if' than between nested and
> flat switches.

Of course, this might be partially because we *have* chains of if else now, but 
no switches on nested patterns.  Still, I agree that the if-else refactor case 
is important.  To that point, a total pattern in a switch gets translated as 
`else` rather than `else if`, and it would be nice if the IDE recognized `case 
null, Object o` as being total in this case, and a null-hostile switch needs 
the implicit null check translated when going from switch -> if else chain.  

It would be great to write down the refactoring asymmetries in one place, just 
to see a “total" picture of how distortive any given treatment is?



Re: Treatment of total patterns (was: Reviewing feedback on patterns in switch)

2022-01-25 Thread Remi Forax
- Original Message -
> From: "Tagir Valeev" 
> To: "Brian Goetz" 
> Cc: "amber-spec-experts" 
> Sent: Wednesday, January 26, 2022 5:20:24 AM
> Subject: Re: Treatment of total patterns (was: Reviewing feedback on patterns 
> in switch)

>> Null is only matched by a switch case that includes `case null`.  Switches 
>> with
>> no `case null` are treated as if they have a `case null: throw NPE`.  This
>> means that `case Object o` doesn’t match null; only `case null, Object o` 
>> does.
>> Total patterns are re-allowed in instanceof expressions, and are consistent 
>> with
>> their legacy form.
> 
> I strongly support this change.
> 
> In my experience, it's much more important to have automatic
> refactorings between switch and chains of 'if' than between nested and
> flat switches. People have chains of 'if's very often and they are not
> legacy. Sometimes, you want to add conditions unrelated to the
> selector expression, so it could be natural to convert 'switch' to
> 'if'. In other cases, you simplify the chain of 'if' statements and
> see that the new set of conditions nicely fits into a pattern switch.
> These if<->switch conversions will be an everyday tool for developers.
> In contrast, destructuring with a switch will be a comparatively rare
> thing, and it's even more rare when you need to convert nested
> switches to flat ones or vice versa. I'm saying this from my Kotlin
> programming experience where you can have when-is and sort of
> destructuring of data classes which are roughly similar to what we are
> doing for Java. One level 'when' is common, two-level 'when' or
> conditions on destructuring components are more rare.
> 
> We already implemented some kind of switch<->if conversion in IntelliJ
> IDEA. And it already has a number of corner cases to handle in order
> to support total patterns that match null. In particular, we cannot
> convert `case Object obj` to `if (x instanceof Object obj), as total
> patterns are prohibited for instanceof and null won't be matched
> anyway. We cannot just omit a condition, as `obj` could be used
> afterwards, so we have to explicitly declare a variable (and I
> believe, this part is still buggy and may produce incompilable code).
> The proposed change will make switch<->if refactorings more mechanical
> and predictable.
> 
> Another thing I mentioned before and want to stress again is that this
> change will allow us to infer required nullity for the variable used
> in the switch selector from AST only. No need to use resolution or
> type inference. This will make interprocedural analysis stronger.
> E.g., consider:
> // Test.java
> class Test {
>  static void test(A a) {
>switch(a) {
>case B b -> {}
>case C c -> {}
>case D d -> {}
>}
>  }
> }
> 
> There are two possibilities:
> 1. D is a superclass of A, thus the last pattern is total, and null is
> accepted here:
> 
> interface D {}
> interface A extends D {}
> interface B extends A {}
> interface C extends A {}
> 
> 2. A is a sealed type with B, C, and D inheritors, switch is
> exhaustive, and null is not accepted here:
> 
> sealed interface A {}
> non-sealed interface B extends A {}
> non-sealed interface C extends A {}
> non-sealed interface D extends A {}
> 
> So without looking at A definition (which might be anywhere) we cannot
> say whether test(null) will throw NPE or not. We cannot cache the
> knowledge about 'test' method parameter nullability within the
> Test.java, because its nullability might change if we change the
> hierarchy of A, even if Test.java content is untouched. Currently, we
> are conservative and not infer nullability when any unguarded pattern
> appears in switch cases. With the required `case null`, we can perform
> more precise analysis.


We should go a step further, this also means that with

switch(box) {
   case Box(B b) -> {}
   case Box(C c) -> {}
   case Box(D d) -> {}
   }

we have no idea if the switch will accept Box(null) or not.

So the idea that a type behave differently if nested inside a pattern or not is 
not a good one.

> 
> With best regards,
> Tagir Valeev.

regards,
Rémi

> 
> On Wed, Jan 26, 2022 at 2:47 AM Brian Goetz  wrote:
>>
>>
>> 1.  Treatment of total patterns in switch / instanceof
>>
>>
>> The handling of totality has been a long and painful discussion, trying to
>> balance between where we want this feature to land in the long term, and
>> people’s existing mental models of what switch and instanceof are supposed to
>> do.  Because we’ve made the conscious decision to rehabilitate switch rather
>> than make a new construct (which would live side by side with the old 
>> construct
>> forever), we have to take into account the preconceived mental models to a
>> greater degree.
>>
>> Totality is a predicate on a pattern and the static type of its match target;
>> for a pattern P to be total on T, it means that all values of T are matched 
>> by
>> P.  Note that when I say “matched by”, I am appealing not necessarily 

Re: Treatment of total patterns (was: Reviewing feedback on patterns in switch)

2022-01-25 Thread Tagir Valeev
> Null is only matched by a switch case that includes `case null`.  Switches 
> with no `case null` are treated as if they have a `case null: throw NPE`.  
> This means that `case Object o` doesn’t match null; only `case null, Object 
> o` does.
> Total patterns are re-allowed in instanceof expressions, and are consistent 
> with their legacy form.

I strongly support this change.

In my experience, it's much more important to have automatic
refactorings between switch and chains of 'if' than between nested and
flat switches. People have chains of 'if's very often and they are not
legacy. Sometimes, you want to add conditions unrelated to the
selector expression, so it could be natural to convert 'switch' to
'if'. In other cases, you simplify the chain of 'if' statements and
see that the new set of conditions nicely fits into a pattern switch.
These if<->switch conversions will be an everyday tool for developers.
In contrast, destructuring with a switch will be a comparatively rare
thing, and it's even more rare when you need to convert nested
switches to flat ones or vice versa. I'm saying this from my Kotlin
programming experience where you can have when-is and sort of
destructuring of data classes which are roughly similar to what we are
doing for Java. One level 'when' is common, two-level 'when' or
conditions on destructuring components are more rare.

We already implemented some kind of switch<->if conversion in IntelliJ
IDEA. And it already has a number of corner cases to handle in order
to support total patterns that match null. In particular, we cannot
convert `case Object obj` to `if (x instanceof Object obj), as total
patterns are prohibited for instanceof and null won't be matched
anyway. We cannot just omit a condition, as `obj` could be used
afterwards, so we have to explicitly declare a variable (and I
believe, this part is still buggy and may produce incompilable code).
The proposed change will make switch<->if refactorings more mechanical
and predictable.

Another thing I mentioned before and want to stress again is that this
change will allow us to infer required nullity for the variable used
in the switch selector from AST only. No need to use resolution or
type inference. This will make interprocedural analysis stronger.
E.g., consider:
// Test.java
class Test {
  static void test(A a) {
switch(a) {
case B b -> {}
case C c -> {}
case D d -> {}
}
  }
}

There are two possibilities:
1. D is a superclass of A, thus the last pattern is total, and null is
accepted here:

interface D {}
interface A extends D {}
interface B extends A {}
interface C extends A {}

2. A is a sealed type with B, C, and D inheritors, switch is
exhaustive, and null is not accepted here:

sealed interface A {}
non-sealed interface B extends A {}
non-sealed interface C extends A {}
non-sealed interface D extends A {}

So without looking at A definition (which might be anywhere) we cannot
say whether test(null) will throw NPE or not. We cannot cache the
knowledge about 'test' method parameter nullability within the
Test.java, because its nullability might change if we change the
hierarchy of A, even if Test.java content is untouched. Currently, we
are conservative and not infer nullability when any unguarded pattern
appears in switch cases. With the required `case null`, we can perform
more precise analysis.

With best regards,
Tagir Valeev.

On Wed, Jan 26, 2022 at 2:47 AM Brian Goetz  wrote:
>
>
> 1.  Treatment of total patterns in switch / instanceof
>
>
> The handling of totality has been a long and painful discussion, trying to 
> balance between where we want this feature to land in the long term, and 
> people’s existing mental models of what switch and instanceof are supposed to 
> do.  Because we’ve made the conscious decision to rehabilitate switch rather 
> than make a new construct (which would live side by side with the old 
> construct forever), we have to take into account the preconceived mental 
> models to a greater degree.
>
> Totality is a predicate on a pattern and the static type of its match target; 
> for a pattern P to be total on T, it means that all values of T are matched 
> by P.  Note that when I say “matched by”, I am appealing not necessarily to 
> “what does switch do” or “what does instanceof do”, but to an abstract notion 
> of matching.
>
> The main place where there is a gap between pattern totality and whether a 
> pattern matches in a switch has to do with null.  We’ve done a nice job 
> retrofitting “case null” onto switch (especially with `case null, Foo f` 
> which allows the null to be bound to f), but people are still uncomfortable 
> with `case Object o` binding null to o.
>
> (Another place there is a gap is with nested patterns; Box(Bag(String s)) 
> should be total on Box>, but can’t match Box(null).   We don’t 
> want to force users to add default cases, but a switch on Box> 
> would need an implicit throwing case to deal with the remainder.)
>
> I 

Re: Treatment of total patterns (was: Reviewing feedback on patterns in switch)

2022-01-25 Thread Remi Forax
> From: "Brian Goetz" 
> To: "amber-spec-experts" 
> Sent: Tuesday, January 25, 2022 8:47:09 PM
> Subject: Treatment of total patterns (was: Reviewing feedback on patterns in
> switch)

>> 1. Treatment of total patterns in switch / instanceof

> The handling of totality has been a long and painful discussion, trying to
> balance between where we want this feature to land in the long term, and
> people’s existing mental models of what switch and instanceof are supposed to
> do. Because we’ve made the conscious decision to rehabilitate switch rather
> than make a new construct (which would live side by side with the old 
> construct
> forever), we have to take into account the preconceived mental models to a
> greater degree.

> Totality is a predicate on a pattern and the static type of its match target;
> for a pattern P to be total on T, it means that all values of T are matched by
> P. Note that when I say “matched by”, I am appealing not necessarily to “what
> does switch do” or “what does instanceof do”, but to an abstract notion of
> matching.

> The main place where there is a gap between pattern totality and whether a
> pattern matches in a switch has to do with null. We’ve done a nice job
> retrofitting “case null” onto switch (especially with `case null, Foo f` which
> allows the null to be bound to f), but people are still uncomfortable with
> `case Object o` binding null to o.

> (Another place there is a gap is with nested patterns; Box(Bag(String s)) 
> should
> be total on Box>, but can’t match Box(null). We don’t want to 
> force
> users to add default cases, but a switch on Box> would need an
> implicit throwing case to deal with the remainder.)

> I am not inclined to reopen the “should `Object o` be total” discussion; I
> really don’t think there’s anything new to say there. But we can refine the
> interaction between a total pattern and what the switch and instanceof
> constructs might do. Just because `Object o` is total on Object, doesn’t mean
> `case Object o` has to match it. It is the latter I am suggesting we might
> reopen.

> The motivation for treating total patterns as total (and therefore nullable) 
> in
> switch comes in part from the desire to avoid introducing sharp edges into
> refactoring. Specifically, we had two invariants in mind:

> x matches P(Q) === x matches P(var alpha) && alpha matches Q:

> and

> switch (x) {
> case P(Q): A
> case P(T): B
> }

> where T is total on the type of x, should be equivalent to

> switch (x) {
> case P(var alpha):
> switch(alpha) {
> case Q: A
> case T: B
> }
> }
> }

> These invariants are powerful both for linguistic transformation and for
> refactoring.

> The refinements I propose are:

> - Null is only matched by a switch case that includes `case null`. Switches 
> with
> no `case null` are treated as if they have a `case null: throw NPE`. This 
> means
> that `case Object o` doesn’t match null; only `case null, Object o` does.

> - Total patterns are re-allowed in instanceof expressions, and are consistent
> with their legacy form.

> Essentially, this gives switch and instanceof a chance to treat null specially
> with their existing semantics, which takes precedence over the pattern match.

> The consequences of this for our refactoring rules are:

> - When unrolling a nested pattern P(Q), we can only do so when Q is not total.
> - When unrolling a switch over nested patterns to a nested switch, `case P(T)`
> must be unrolled not to `case T`, but `case null, T`.

> These changes entail no changes to the semantics of pattern matching; they are
> changes to the semantics of instanceof/switch with regard to null.

I have a slight preference for the C# syntax, the only way to have a total 
pattern is to use "var" so case P(T) is equivalent to instanceof P p && p.t 
instanceof T t. 
Yes, it's not great because it means that "var" is not just inference but i 
think i prefer that compromise than having a type in a pattern means something 
different if it is nested or not. 

The semantics you are proposing (or the one currently implemented in Java 18) 
is objectively neither worst nor better than the C# one, it's just different. 
Pragmatically, we should choose the C# semantics, just because there are 
already thousands of people who knows it. 

Rémi 


Treatment of total patterns (was: Reviewing feedback on patterns in switch)

2022-01-25 Thread Brian Goetz

1.  Treatment of total patterns in switch / instanceof

The handling of totality has been a long and painful discussion, trying to 
balance between where we want this feature to land in the long term, and 
people’s existing mental models of what switch and instanceof are supposed to 
do.  Because we’ve made the conscious decision to rehabilitate switch rather 
than make a new construct (which would live side by side with the old construct 
forever), we have to take into account the preconceived mental models to a 
greater degree.

Totality is a predicate on a pattern and the static type of its match target; 
for a pattern P to be total on T, it means that all values of T are matched by 
P.  Note that when I say “matched by”, I am appealing not necessarily to “what 
does switch do” or “what does instanceof do”, but to an abstract notion of 
matching.

The main place where there is a gap between pattern totality and whether a 
pattern matches in a switch has to do with null.  We’ve done a nice job 
retrofitting “case null” onto switch (especially with `case null, Foo f` which 
allows the null to be bound to f), but people are still uncomfortable with 
`case Object o` binding null to o.

(Another place there is a gap is with nested patterns; Box(Bag(String s)) 
should be total on Box>, but can’t match Box(null).   We don’t want 
to force users to add default cases, but a switch on Box> would 
need an implicit throwing case to deal with the remainder.)

I am not inclined to reopen the “should `Object o` be total” discussion; I 
really don’t think there’s anything new to say there.  But we can refine the 
interaction between a total pattern and what the switch and instanceof 
constructs might do.  Just because `Object o` is total on Object, doesn’t mean 
`case Object o` has to match it.  It is the latter I am suggesting we might 
reopen.

The motivation for treating total patterns as total (and therefore nullable) in 
switch comes in part from the desire to avoid introducing sharp edges into 
refactoring.  Specifically, we had two invariants in mind:

x matches P(Q) === x matches P(var alpha) && alpha matches Q:

and

switch (x) {
case P(Q): A
case P(T): B
}

where T is total on the type of x, should be equivalent to

switch (x) {
case P(var alpha):
switch(alpha) {
case Q: A
case T: B
}
}
   }

These invariants are powerful both for linguistic transformation and for 
refactoring.

The refinements I propose are:

 - Null is only matched by a switch case that includes `case null`.  Switches 
with no `case null` are treated as if they have a `case null: throw NPE`.  This 
means that `case Object o` doesn’t match null; only `case null, Object o` does.

 - Total patterns are re-allowed in instanceof expressions, and are consistent 
with their legacy form.

Essentially, this gives switch and instanceof a chance to treat null specially 
with their existing semantics, which takes precedence over the pattern match.

The consequences of this for our refactoring rules are:

 - When unrolling a nested pattern P(Q), we can only do so when Q is not total.
 - When unrolling a switch over nested patterns to a nested switch, `case P(T)` 
must be unrolled not to `case T`, but `case null, T`.


These changes entail no changes to the semantics of pattern matching; they are 
changes to the semantics of instanceof/switch with regard to null.