Re: [External] : Re: Reviewing feedback on patterns in switch
> On Feb 16, 2022, at 9:57 AM, Brian Goetz wrote: > . . . > What if I want to use a partial pattern, and then customize either the > throwing part or provide default values? I can provide an else clause: > > Object o = ... > let String s = o > else throw new NotStringException(); > > or > > Object o = ... > let String s = o > else { s = "no string"; } Thanks for these examples; I had forgotten exactly what was previous proposed. > Reminder: THIS EXPLANATION WAS PROVIDED SOLELY TO CLARIFY THE "FUTURE > CONSTRUCT" COMMENT IN THE && DISCUSSION. YES; I’M DONE NOW, THANKS! :-)
Re: [External] : Re: Reviewing feedback on patterns in switch
> From: "Brian Goetz" > To: "Remi Forax" > Cc: "amber-spec-experts" > Sent: Wednesday, February 16, 2022 5:03:19 PM > Subject: Re: [External] : Re: Reviewing feedback on patterns in switch > Of course, in an ecosystem as diverse as Java developers, one routinely > expects > to get complaints about both X and ~X. Which makes it notable that we have not > gotten any complaints about "why do you force me to write an empty default". > (I'm not complaining!) > The case you raise -- legacy { switch type, labels, statement } switches -- is > harder to fix. The things we've explored (like an opt-in to totality) are > pretty poor fixes, since (a) they are noisy warts, and (b) people will forget > them and still have the problem. So these are harder, longer-term problems. > (For now, the best we can do is noisy warnings.) The problem of the noisy warning is that there is no "right" way to fix the warning now. Adding a default is not what we want, we need a way to opt-in exhaustiveness. But we never agree on the way to do that. Rémi > On 2/16/2022 11:00 AM, Remi Forax wrote: >>> From: "Brian Goetz" [ mailto:brian.go...@oracle.com | >>> ] >>> To: "amber-spec-experts" [ mailto:amber-spec-experts@openjdk.java.net | >>> ] >>> Sent: Wednesday, February 16, 2022 4:49:19 PM >>> Subject: Re: Reviewing feedback on patterns in switch >>> One thing that we have, perhaps surprisingly, *not* gotten feedback on is >>> forcing all non-legacy switches (legacy type, legacy labels, statement >>> only) to >>> be exhaustive. I would have thought people would complain about pattern >>> switches needing to be exhaustive, but no one has! So either no one has >>> tried >>> it, or we got away with it... >> Yes, we had several feedbacks about the opposite, why the switch statement >> on an >> enum is not exhaustive, i.e. why the following code does not compile >> enum Color { RED , BLUE } int x; >> Color color = null ; switch (color) { case RED -> x = 0 ; case BLUE -> x = 1 >> ; >> } >> System. out .println(x); // x may not be initialized >> Rémi >>> On 1/25/2022 2:46 PM, Brian Goetz wrote: >>>> We’ve previewed patterns in switch for two rounds, and have received some >>>> feedback. Overall, things work quite well, but there were a few items >>>> which >>>> received some nontrivial feedback, and I’m prepared to suggest some changes >>>> based on them. I’ll summarize them here and create a new thread for each >>>> with >>>> a more detailed description. >>>> I’ll make a call for additional items a little later; for now, let’s focus >>>> on >>>> these items before adding new things (or reopening old ones.) >>>> 1. Treatment of total patterns in switch / instanceof >>>> 2. Positioning of guards >>>> 3. Type refinements for GADTs >>>> 4. Diamond for type patterns (and record patterns)
Re: [External] : Re: Reviewing feedback on patterns in switch
> From: "Brian Goetz" > To: "Remi Forax" > Cc: "amber-spec-experts" > Sent: Wednesday, February 16, 2022 3:57:39 PM > Subject: Re: [External] : Re: Reviewing feedback on patterns in switch > OK, I'll make you a deal: I'll answer your question about let/bind, under the > condition that we not divert the discussion on that right now -- there'll be a > proper writeup soon. The answer here is entirely for context. > If you don't agree, stop reading now :) I think it's wiser to delay the discussion about let for later :) Rémi > On 2/15/2022 5:58 PM, Remi Forax wrote: >>> - There are future constructs that may take patterns, and may (or may not) >>> want >>> to express guard-like behavior, such as `let` statements (e.g., let .. when >>> .. >>> else.) Expressing guards here with && is even less evocative of "guard >>> condition" than it is with switches. >> It's not clear to me how to use "let when else". Is it more like a ?: in C >> than >> the let in in Caml ? > The simplest form of `let` is a statement that takes a total pattern: > let Point(var x, var y) = aPoint; > and introduces bindings x and y into the remainder of the block. When > applicable, this is better than a conditional context because (a) you get type > checking for totality, and (b) you don't indent the rest of your method inside > a test that you know will always succeed. > If the pattern is total but has some remainder, the construct must throw on > the > remainder, to preserve the invariant that when a `let` statement completes > normally, all bindings are DA. > What if I want to use a partial pattern, and then customize either the > throwing > part or provide default values? I can provide an else clause: > Object o = ... > let String s = o > else throw new NotStringException(); > or > Object o = ... > let String s = o > else { s = "no string"; } > These are two ways to preserve the "DA on normal completion" invariant; either > by not completing normally, or by ensuring the bindings are DA. > Now, we are in a situation where we are with switch: patterns do not express > all > possible conditions. Which is why we introduced guards to switches. And we can > use the same trick here: > Object o = ... > let String s = o > when (!s.isEmpty()) > else { s = "no string"; } > If we tried to use && here, it would look like > Object o = ... > let String s = o && (!s.isEmpty()) > else { s = "no string"; } > which has the same problem as `case false && false`. > Reminder: THIS EXPLANATION WAS PROVIDED SOLELY TO CLARIFY THE "FUTURE > CONSTRUCT" > COMMENT IN THE && DISCUSSION.
Re: [External] : Re: Reviewing feedback on patterns in switch
> From: "Brian Goetz" > To: "Remi Forax" > Cc: "amber-spec-experts" > Sent: Wednesday, February 16, 2022 3:48:14 PM > Subject: Re: [External] : Re: Reviewing feedback on patterns in switch >> Not sure it's a no-brainer. >> The question is more a question of consistency. There are two consistencies >> and >> we have to choose one, either switch never allows null by default and users >> have to opt-in with case null or we want patterns to behave the same way if >> they are declared at top-level or if they are nested. I would say that the >> semantics you propose is more like the current Java and the other semantics >> is >> more like the Java of a future (if we choose the second option). > You are right that any justification involving "for consistency" is mostly a > self-justification. But here's where I think this is a cleaner decomposition. > We define the semantics of the patterns in a vacuum; matching is a three-place > predicate involving a static target type, a target expression, and a pattern. > Null is not special here. (This is how we've done this all along.) > Pattern contexts (instanceof, switch, and in the future, nested patterns, > let/bind, catch, etc) on the other hand, may have pre-existing (and in some > cases reasonable) opinions about null. What's new here is to fully separate > the > construct opinions about special values from the pattern semantics -- the > construct makes its decision about the special values, before consulting the > pattern. > This lets instanceof treat null as valid but say "null is not an instance of > anything", past-switch treats null as always an error, and future-switch > treats > null as a value you can opt into matching with the `null` label. (Yes, this is > clunky; if we had non-nullable type patterns, we'd get there more directly.) > But the part that I think is more or less obvious-in-hindsight is that the > switch opinions are switches opinions, and the pattern opinions are pattern > opinions, and there is a well-defined order in which those opinions are acted > on -- the construct mediates between the target and the patterns. That is, we > compose the result from the construct semantics and-then the pattern > semantics. I think it will be more clear when we will introduce patterns on local variable declaration because those will only allow some patterns but not all. > None of this is really all that much about "how do people like it". But what I > do think people will like is that they get a simple rule out of switches: > "switches throw on null unless the letters n-u-l-l appear in the switch body". > And a simple rule for instanceof: "instanceof never evaluates to true on > null". > And that these rules are *independent of patterns*. So switch and instanceof > can be understood separately from patterns. It's not about how people like it but how people rationalize it. You can say "switches throw on null unless the letters n-u-l-l appear in the switch body" or "switches throw on null unless a null-friendly pattern appear in the switch body and this is also true for nested patterns". Both are valid approach. Rémi
Re: Reviewing feedback on patterns in switch
On 2022-02-16T10:49:19 -0500 Brian Goetz wrote: > One thing that we have, perhaps surprisingly, *not* gotten feedback on > is forcing all non-legacy switches (legacy type, legacy labels, > statement only) to be exhaustive. I would have thought people would > complain about pattern switches needing to be exhaustive, but no one > has! So either no one has tried it, or we got away with it... > I tried it and liked it. It didn't go unnoticed. :) -- Mark Raynsford | https://www.io7m.com
Re: Reviewing feedback on patterns in switch
> From: "Brian Goetz" > To: "amber-spec-experts" > Sent: Wednesday, February 16, 2022 4:49:19 PM > Subject: Re: Reviewing feedback on patterns in switch > One thing that we have, perhaps surprisingly, *not* gotten feedback on is > forcing all non-legacy switches (legacy type, legacy labels, statement only) > to > be exhaustive. I would have thought people would complain about pattern > switches needing to be exhaustive, but no one has! So either no one has tried > it, or we got away with it... Yes, we had several feedbacks about the opposite, why the switch statement on an enum is not exhaustive, i.e. why the following code does not compile enum Color { RED , BLUE } int x; Color color = null ; switch (color) { case RED -> x = 0 ; case BLUE -> x = 1 ; } System. out .println(x); // x may not be initialized Rémi > On 1/25/2022 2:46 PM, Brian Goetz wrote: >> We’ve previewed patterns in switch for two rounds, and have received some >> feedback. Overall, things work quite well, but there were a few items which >> received some nontrivial feedback, and I’m prepared to suggest some changes >> based on them. I’ll summarize them here and create a new thread for each >> with >> a more detailed description. >> I’ll make a call for additional items a little later; for now, let’s focus on >> these items before adding new things (or reopening old ones.) >> 1. Treatment of total patterns in switch / instanceof >> 2. Positioning of guards >> 3. Type refinements for GADTs >> 4. Diamond for type patterns (and record patterns)
Re: Reviewing feedback on patterns in switch
> From: "Brian Goetz" > To: "Guy Steele" , "Remi Forax" > Cc: "amber-spec-experts" > Sent: Wednesday, February 16, 2022 4:05:35 PM > Subject: Re: Reviewing feedback on patterns in switch >>> For me, && is more natural than "when" because i've written more switch that >>> uses && than "when". >>> And don't forget that unlike most of the code, with pattern matching the >>> number >>> of characters does matter, this is more similar to lambdas, if what you >>> write >>> is too verbose, you will not write it. >> At the risk of premature bikeshedding, have we already discussed and >> discarded >> the idea of spelling “when” as “if”? It’s been a long time, and I forget. > There was not extensive discussion on this, and its all very > subjective/handwavy/"what we think people would think", but I remember a few > comments on this: > - The generality of "if" reminded people of the Perl-style "statement unless > condition" postfix convention, and that people might see it as an > "inconsistency" that they could not then say > x = 3 if (condition); > which is definitely somewhere we don't want to go. > - We're use to seeing "if" with a consequence, and a "naked" if might have the > effect of "lookahead pollution" in our mental parsers. > - Keeping `if` for statements allows us to keep the "body" of case clauses > visually distinct from the "envelope": > case Foo(var x) > if (x > 3) : if (x > 10) { ... } > would make people's eyes go buggy. One could argue that "when" is not > fantastically better: > case Foo(var x) > when (x > 3) : if (x > 10) { ... } > but it doesn't take quite as long to de-bug oneself in that case. And also the if stis followed by parenthesis and there is no need of parenthesis for a guard. So either people will always put parenthesis after if as a guard or be mystify that parenthesis are not required for a guard but required for the if statement. Rémi
Re: [External] : Re: Reviewing feedback on patterns in switch
Of course, in an ecosystem as diverse as Java developers, one routinely expects to get complaints about both X and ~X. Which makes it notable that we have not gotten any complaints about "why do you force me to write an empty default". (I'm not complaining!) The case you raise -- legacy { switch type, labels, statement } switches -- is harder to fix. The things we've explored (like an opt-in to totality) are pretty poor fixes, since (a) they are noisy warts, and (b) people will forget them and still have the problem. So these are harder, longer-term problems. (For now, the best we can do is noisy warnings.) On 2/16/2022 11:00 AM, Remi Forax wrote: *From: *"Brian Goetz" *To: *"amber-spec-experts" *Sent: *Wednesday, February 16, 2022 4:49:19 PM *Subject: *Re: Reviewing feedback on patterns in switch One thing that we have, perhaps surprisingly, *not* gotten feedback on is forcing all non-legacy switches (legacy type, legacy labels, statement only) to be exhaustive. I would have thought people would complain about pattern switches needing to be exhaustive, but no one has! So either no one has tried it, or we got away with it... Yes, we had several feedbacks about the opposite, why the switch statement on an enum is not exhaustive, i.e. why the following code does not compile enum Color {RED,BLUE } int x; Color color =null; switch (color) { case RED -> x =0; case BLUE -> x =1; } System.out.println(x); // x may not be initialized Rémi On 1/25/2022 2:46 PM, Brian Goetz wrote: We’ve previewed patterns in switch for two rounds, and have received some feedback. Overall, things work quite well, but there were a few items which received some nontrivial feedback, and I’m prepared to suggest some changes based on them. I’ll summarize them here and create a new thread for each with a more detailed description. I’ll make a call for additional items a little later; for now, let’s focus on these items before adding new things (or reopening old ones.) 1. Treatment of total patterns in switch / instanceof 2. Positioning of guards 3. Type refinements for GADTs 4. Diamond for type patterns (and record patterns)
Re: Reviewing feedback on patterns in switch
One thing that we have, perhaps surprisingly, *not* gotten feedback on is forcing all non-legacy switches (legacy type, legacy labels, statement only) to be exhaustive. I would have thought people would complain about pattern switches needing to be exhaustive, but no one has! So either no one has tried it, or we got away with it... On 1/25/2022 2:46 PM, Brian Goetz wrote: We’ve previewed patterns in switch for two rounds, and have received some feedback. Overall, things work quite well, but there were a few items which received some nontrivial feedback, and I’m prepared to suggest some changes based on them. I’ll summarize them here and create a new thread for each with a more detailed description. I’ll make a call for additional items a little later; for now, let’s focus on these items before adding new things (or reopening old ones.) 1. Treatment of total patterns in switch / instanceof 2. Positioning of guards 3. Type refinements for GADTs 4. Diamond for type patterns (and record patterns)
Re: Reviewing feedback on patterns in switch
For me, && is more natural than "when" because i've written more switch that uses && than "when". And don't forget that unlike most of the code, with pattern matching the number of characters does matter, this is more similar to lambdas, if what you write is too verbose, you will not write it. At the risk of premature bikeshedding, have we already discussed and discarded the idea of spelling “when” as “if”? It’s been a long time, and I forget. There was not extensive discussion on this, and its all very subjective/handwavy/"what we think people would think", but I remember a few comments on this: - The generality of "if" reminded people of the Perl-style "statement unless condition" postfix convention, and that people might see it as an "inconsistency" that they could not then say x = 3 if (condition); which is definitely somewhere we don't want to go. - We're use to seeing "if" with a consequence, and a "naked" if might have the effect of "lookahead pollution" in our mental parsers. - Keeping `if` for statements allows us to keep the "body" of case clauses visually distinct from the "envelope": case Foo(var x) if (x > 3) : if (x > 10) { ... } would make people's eyes go buggy. One could argue that "when" is not fantastically better: case Foo(var x) when (x > 3) : if (x > 10) { ... } but it doesn't take quite as long to de-bug oneself in that case. On 2/15/2022 9:55 PM, Guy Steele wrote:
Re: [External] : Re: Reviewing feedback on patterns in switch
OK, I'll make you a deal: I'll answer your question about let/bind, under the condition that we not divert the discussion on that right now -- there'll be a proper writeup soon. The answer here is entirely for context. If you don't agree, stop reading now :) On 2/15/2022 5:58 PM, Remi Forax wrote: - There are future constructs that may take patterns, and may (or may not) want to express guard-like behavior, such as `let` statements (e.g., let .. when .. else.) Expressing guards here with && is even less evocative of "guard condition" than it is with switches. It's not clear to me how to use "let when else". Is it more like a ?: in C than the let in in Caml ? The simplest form of `let` is a statement that takes a total pattern: let Point(var x, var y) = aPoint; and introduces bindings x and y into the remainder of the block. When applicable, this is better than a conditional context because (a) you get type checking for totality, and (b) you don't indent the rest of your method inside a test that you know will always succeed. If the pattern is total but has some remainder, the construct must throw on the remainder, to preserve the invariant that when a `let` statement completes normally, all bindings are DA. What if I want to use a partial pattern, and then customize either the throwing part or provide default values? I can provide an else clause: Object o = ... let String s = o else throw new NotStringException(); or Object o = ... let String s = o else { s = "no string"; } These are two ways to preserve the "DA on normal completion" invariant; either by not completing normally, or by ensuring the bindings are DA. Now, we are in a situation where we are with switch: patterns do not express all possible conditions. Which is why we introduced guards to switches. And we can use the same trick here: Object o = ... let String s = o when (!s.isEmpty()) else { s = "no string"; } If we tried to use && here, it would look like Object o = ... let String s = o && (!s.isEmpty()) else { s = "no string"; } which has the same problem as `case false && false`. Reminder: THIS EXPLANATION WAS PROVIDED SOLELY TO CLARIFY THE "FUTURE CONSTRUCT" COMMENT IN THE && DISCUSSION.
Re: [External] : Re: Reviewing feedback on patterns in switch
Not sure it's a no-brainer. The question is more a question of consistency. There are two consistencies and we have to choose one, either switch never allows null by default and users have to opt-in with case null or we want patterns to behave the same way if they are declared at top-level or if they are nested. I would say that the semantics you propose is more like the current Java and the other semantics is more like the Java of a future (if we choose the second option). You are right that any justification involving "for consistency" is mostly a self-justification. But here's where I think this is a cleaner decomposition. We define the semantics of the patterns in a vacuum; matching is a three-place predicate involving a static target type, a target expression, and a pattern. Null is not special here. (This is how we've done this all along.) Pattern contexts (instanceof, switch, and in the future, nested patterns, let/bind, catch, etc) on the other hand, may have pre-existing (and in some cases reasonable) opinions about null. What's new here is to fully separate the construct opinions about special values from the pattern semantics -- the construct makes its decision about the special values, before consulting the pattern. This lets instanceof treat null as valid but say "null is not an instance of anything", past-switch treats null as always an error, and future-switch treats null as a value you can opt into matching with the `null` label. (Yes, this is clunky; if we had non-nullable type patterns, we'd get there more directly.) But the part that I think is more or less obvious-in-hindsight is that the switch opinions are switches opinions, and the pattern opinions are pattern opinions, and there is a well-defined order in which those opinions are acted on -- the construct mediates between the target and the patterns. That is, we compose the result from the construct semantics and-then the pattern semantics. None of this is really all that much about "how do people like it". But what I do think people will like is that they get a simple rule out of switches: "switches throw on null unless the letters n-u-l-l appear in the switch body". And a simple rule for instanceof: "instanceof never evaluates to true on null". And that these rules are *independent of patterns*. So switch and instanceof can be understood separately from patterns.
Re: Reviewing feedback on patterns in switch
On Feb 15, 2022, at 5:58 PM, Remi Forax mailto:fo...@univ-mlv.fr>> wrote: From: "Brian Goetz" mailto:brian.go...@oracle.com>> To: "amber-spec-experts" mailto:amber-spec-experts@openjdk.java.net>> Sent: Tuesday, February 15, 2022 7:50:06 PM Subject: Re: Reviewing feedback on patterns in switch We're preparing a third preview of type patterns in switch. Normally we would release after a second preview, but (a) we're about to get record patterns, which may disclose additional issues with switch, so best to keep it open for at least another round, and (b) we're proposing some nontrivial changes which deserve another preview. Here's where we are on these. 1. Treatment of total patterns in switch / instanceof Quite honestly, in hindsight, I don't know why we didn't see this sooner; the incremental evolution proposed here is more principled than where we were in the previous round; now the construct (instanceof, switch, etc) *always* gets first crack at enforcing its nullity (and exception) opinions, and *then* delegates to the matching semantics of the pattern if it decides to do so. This fully separates pattern semantics from conditional construct semantics, rather than complecting them (which in turn deprived users of seeing the model more clearly.) In hindsight, this is a no-brainer (which is why we preview things.) We'll be addressing this in the 3rd preview. Not sure it's a no-brainer. The question is more a question of consistency. There are two consistencies and we have to choose one, either switch never allows null by default and users have to opt-in with case null or we want patterns to behave the same way if they are declared at top-level or if they are nested. I would say that the semantics you propose is more like the current Java and the other semantics is more like the Java of a future (if we choose the second option). I think we should try the semantics you propose and see if people agree or not. And I agree we should try these semantics. 2. Positioning of guards Making guards part of switch also feels like a better factoring than making them part of patterns; it simplifies patterns and totality, and puts switch on a more equal footing with our other conditional constructs. We did go back and forth a few times on this, but having given this a few weeks to settle, I'm pretty convinced we'd regret going the other way. There were two sub-points here: (a) is the guard part of the pattern or part of switch, and (b) the syntax. There was general agreement on (a), but some had preference for && on (b). I spent some more time thinking about this choice, and have come down firmly on the `when` side of the house as a result for a number of reasons. Still agree on (a) - Possibility for ambiguity. If switching over booleans (which we will surely eventually be forced into), locutions like `case false && false` will be very confusing; it's pure puzzler territory. - && has a stronger precedence than keyword-based operators like `instanceof`'; we want guards to be weakest here. I don't understand your point, we want instanceof pattern && expression to be equivalent to instanceof type && expression + cast, so the fact that && has a stronger precedence makes that possible so it's not an issue. - Using && will confuse users about whether it is part of the expression, or part of the switch statement. If we're deciding it is part of the switch, this should be clear, and a `when` clause makes that clear. I don't think it's that important, apart if we start to also want to combine patterns with && - There are future constructs that may take patterns, and may (or may not) want to express guard-like behavior, such as `let` statements (e.g., let .. when .. else.) Expressing guards here with && is even less evocative of "guard condition" than it is with switches. It's not clear to me how to use "let when else". Is it more like a ?: in C than the let in in Caml ? That is what I understood the implication to be: something like let User(var firstname, var lastName) = x when firstName.length() > 8 in System.out.printf(“User with long first name”); else System.out.printf(“Not a user, or user with a short first name”); although this particular example could also be framed as if (x instanceof User(var firstname, var lastName) && firstName.length() > 8) System.out.printf(“User with long first name”); else System.out.printf(“Not a user, or user with a short first name”); so maybe I am misunderstanding something here, or have misremembered the proposal. - Users coming from other languages will find `case...when` quite clear. - We've talked about "targetless" switches as a possible future feature, which express multi-way conditionals: switch {
Re: Reviewing feedback on patterns in switch
> From: "Brian Goetz" > To: "amber-spec-experts" > Sent: Tuesday, February 15, 2022 7:50:06 PM > Subject: Re: Reviewing feedback on patterns in switch > We're preparing a third preview of type patterns in switch. Normally we would > release after a second preview, but (a) we're about to get record patterns, > which may disclose additional issues with switch, so best to keep it open for > at least another round, and (b) we're proposing some nontrivial changes which > deserve another preview. > Here's where we are on these. >> 1. Treatment of total patterns in switch / instanceof > Quite honestly, in hindsight, I don't know why we didn't see this sooner; the > incremental evolution proposed here is more principled than where we were in > the previous round; now the construct (instanceof, switch, etc) *always* gets > first crack at enforcing its nullity (and exception) opinions, and *then* > delegates to the matching semantics of the pattern if it decides to do so. > This > fully separates pattern semantics from conditional construct semantics, rather > than complecting them (which in turn deprived users of seeing the model more > clearly.) In hindsight, this is a no-brainer (which is why we preview things.) > We'll be addressing this in the 3rd preview. Not sure it's a no-brainer. The question is more a question of consistency. There are two consistencies and we have to choose one, either switch never allows null by default and users have to opt-in with case null or we want patterns to behave the same way if they are declared at top-level or if they are nested. I would say that the semantics you propose is more like the current Java and the other semantics is more like the Java of a future (if we choose the second option). I think we should try the semantics you propose and see if people agree or not. >> 2. Positioning of guards > Making guards part of switch also feels like a better factoring than making > them > part of patterns; it simplifies patterns and totality, and puts switch on a > more equal footing with our other conditional constructs. We did go back and > forth a few times on this, but having given this a few weeks to settle, I'm > pretty convinced we'd regret going the other way. > There were two sub-points here: (a) is the guard part of the pattern or part > of > switch, and (b) the syntax. There was general agreement on (a), but some had > preference for && on (b). I spent some more time thinking about this choice, > and have come down firmly on the `when` side of the house as a result for a > number of reasons. Still agree on (a) > - Possibility for ambiguity. If switching over booleans (which we will surely > eventually be forced into), locutions like `case false && false` will be very > confusing; it's pure puzzler territory. > - && has a stronger precedence than keyword-based operators like > `instanceof`'; > we want guards to be weakest here. I don't understand your point, we want instanceof pattern && expression to be equivalent to instanceof type && expression + cast, so the fact that && has a stronger precedence makes that possible so it's not an issue. > - Using && will confuse users about whether it is part of the expression, or > part of the switch statement. If we're deciding it is part of the switch, this > should be clear, and a `when` clause makes that clear. I don't think it's that important, apart if we start to also want to combine patterns with && > - There are future constructs that may take patterns, and may (or may not) > want > to express guard-like behavior, such as `let` statements (e.g., let .. when .. > else.) Expressing guards here with && is even less evocative of "guard > condition" than it is with switches. It's not clear to me how to use "let when else". Is it more like a ?: in C than the let in in Caml ? > - Users coming from other languages will find `case...when` quite clear. > - We've talked about "targetless" switches as a possible future feature, which > express multi-way conditionals: > switch { > case when (today() == TUESDAY): ... > case when (location() == GREENLAND): ... > ... > } > This would look quite silly with &&. For me, this is like cond in Lisp but more verbose. using "case" and "when" here is sillly. > Similarly, one could mix guards with a targeted switch: > switch (x) { > case Time t: ... > case Place p: ... > default when (today() == TUESDAY): ... tuesday-specific default > default: ... regular default ... default && today() == TUESDAY is fine for me. > Expressing guards that are the whole condition with `when` is much more > natural >
Re: Reviewing feedback on patterns in switch
We're preparing a third preview of type patterns in switch. Normally we would release after a second preview, but (a) we're about to get record patterns, which may disclose additional issues with switch, so best to keep it open for at least another round, and (b) we're proposing some nontrivial changes which deserve another preview. Here's where we are on these. 1. Treatment of total patterns in switch / instanceof Quite honestly, in hindsight, I don't know why we didn't see this sooner; the incremental evolution proposed here is more principled than where we were in the previous round; now the construct (instanceof, switch, etc) *always* gets first crack at enforcing its nullity (and exception) opinions, and *then* delegates to the matching semantics of the pattern if it decides to do so. This fully separates pattern semantics from conditional construct semantics, rather than complecting them (which in turn deprived users of seeing the model more clearly.) In hindsight, this is a no-brainer (which is why we preview things.) We'll be addressing this in the 3rd preview. 2. Positioning of guards Making guards part of switch also feels like a better factoring than making them part of patterns; it simplifies patterns and totality, and puts switch on a more equal footing with our other conditional constructs. We did go back and forth a few times on this, but having given this a few weeks to settle, I'm pretty convinced we'd regret going the other way. There were two sub-points here: (a) is the guard part of the pattern or part of switch, and (b) the syntax. There was general agreement on (a), but some had preference for && on (b). I spent some more time thinking about this choice, and have come down firmly on the `when` side of the house as a result for a number of reasons. - Possibility for ambiguity. If switching over booleans (which we will surely eventually be forced into), locutions like `case false && false` will be very confusing; it's pure puzzler territory. - && has a stronger precedence than keyword-based operators like `instanceof`'; we want guards to be weakest here. - Using && will confuse users about whether it is part of the expression, or part of the switch statement. If we're deciding it is part of the switch, this should be clear, and a `when` clause makes that clear. - There are future constructs that may take patterns, and may (or may not) want to express guard-like behavior, such as `let` statements (e.g., let .. when .. else.) Expressing guards here with && is even less evocative of "guard condition" than it is with switches. - Users coming from other languages will find `case...when` quite clear. - We've talked about "targetless" switches as a possible future feature, which express multi-way conditionals: switch { case when (today() == TUESDAY): ... case when (location() == GREENLAND): ... ... } This would look quite silly with &&. Similarly, one could mix guards with a targeted switch: switch (x) { case Time t: ... case Place p: ... default when (today() == TUESDAY): ... tuesday-specific default default: ... regular default ... Expressing guards that are the whole condition with `when` is much more natural than with &&. tl;dr: inventing a `when` modifier on switch now will save us from having to invent something else in the future; choosing && will not. We can continue to discuss the bikeshed at low volume (at least until we start repeating ourselves), but we need to address both of these points in the 3rd preview. 3. Type refinements for GADTs I've been working through the details here, and there are a number of additional touch points where GADTs can provide type refinement, not just on the RHS of a case, such as totality and inference. I'll be pulling all these together to try to get a total picture here. It's not a blocker for the 3rd preview, it can be a future refinement. 4. Diamond for type patterns (and record patterns) This seems desirable, but there are details to work out. It's not a blocker for the 3rd preview, it can be a future refinement.
Re: [External] : Re: Treatment of total patterns (was: Reviewing feedback on patterns in switch)
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)
> 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: Reviewing feedback on patterns in switch
- Original Message - > From: "mark" > To: "Brian Goetz" > Cc: "amber-spec-experts" > Sent: Friday, January 28, 2022 12:43:55 PM > Subject: Re: Reviewing feedback on patterns in switch > On 2022-01-25T19:46:09 + > Brian Goetz wrote: > >> We’ve previewed patterns in switch for two rounds, and have received some >> feedback. Overall, things work quite well, but there were a few items which >> received some nontrivial feedback, and I’m prepared to suggest some changes >> based on them. I’ll summarize them here and create a new thread for each >> with >> a more detailed description. >> >> I’ll make a call for additional items a little later; for now, let’s focus on >> these items before adding new things (or reopening old ones.) >> >> 1. Treatment of total patterns in switch / instanceof >> >> 2. Positioning of guards >> >> 3. Type refinements for GADTs >> >> 4. Diamond for type patterns (and record patterns) > > Hello! > > I'm a little late to the party, as ever, but is there a specific build I > should be looking at so that I can get a better idea of what the current > state of things are? Hi Mark, the last jdk18 build or any new jdk19 builds will do. > > -- > Mark Raynsford | https://www.io7m.com Rémi
Re: Reviewing feedback on patterns in switch
On 2022-01-25T19:46:09 + Brian Goetz wrote: > We’ve previewed patterns in switch for two rounds, and have received some > feedback. Overall, things work quite well, but there were a few items which > received some nontrivial feedback, and I’m prepared to suggest some changes > based on them. I’ll summarize them here and create a new thread for each > with a more detailed description. > > I’ll make a call for additional items a little later; for now, let’s focus on > these items before adding new things (or reopening old ones.) > > 1. Treatment of total patterns in switch / instanceof > > 2. Positioning of guards > > 3. Type refinements for GADTs > > 4. Diamond for type patterns (and record patterns) Hello! I'm a little late to the party, as ever, but is there a specific build I should be looking at so that I can get a better idea of what the current state of things are? -- Mark Raynsford | https://www.io7m.com
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.
Re: [External] : Re: Diamond in type patterns (was: Reviewing feedback on patterns in switch)
OK, I get your point now. Your concern is not about *inference*, but specifically how *diamond* will snap to the bound when it infers a wildcard, because `new` doesn’t allow wildcards. But we do something differently for inferring type variables of generic methods (we’ll gladly infer a capture) or locals (we’ll project a capture to a nearby super type without capture.) On Jan 27, 2022, at 8:20 AM, fo...@univ-mlv.fr<mailto:fo...@univ-mlv.fr> wrote: From: "Brian Goetz" mailto:brian.go...@oracle.com>> To: "Remi Forax" mailto:fo...@univ-mlv.fr>> Cc: "amber-spec-experts" mailto:amber-spec-experts@openjdk.java.net>> Sent: Thursday, January 27, 2022 2:04:35 PM Subject: Re: [External] : Re: Diamond in type patterns (was: Reviewing feedback on patterns in switch) It's more an engineering thing here, we have far more casts than switch + pattern in existing code, and given that we suppose (perhaps wrongly) that the semantics of the inference is not exactly one already existing, I’d like to drill into this supposition. My supposition (maybe wrong) is that we already solved most of this when we did `var`, with upward projection. To recap, we spent a lot of time with `var` on what to do about non-denotable types. These included the null type (banned on the grounds of uselessness), intersection types (allowed), and capture types (sanitized with upward projection.) The basic idea of upward projection is that when we infer List, we replace it with a super type that has no capture types, and get List out. (There’s also a downward projection.) Let’s start with your examples of where ordinary inference produces an undesirable result, and then evaluate whether either or the projections solves the problem? I don't think current projections are enough because we may want the inference to insert a wildcard by itsef, for example with Object o = ... var list = (List<>) o; or maybe we should not try to infer such code. Rémi
Re: [External] : Re: Diamond in type patterns (was: Reviewing feedback on patterns in switch)
> From: "Brian Goetz" > To: "Remi Forax" > Cc: "amber-spec-experts" > Sent: Thursday, January 27, 2022 2:04:35 PM > Subject: Re: [External] : Re: Diamond in type patterns (was: Reviewing > feedback > on patterns in switch) >> It's more an engineering thing here, we have far more casts than switch + >> pattern in existing code, and given that we suppose (perhaps wrongly) that >> the >> semantics of the inference is not exactly one already existing, > I’d like to drill into this supposition. My supposition (maybe wrong) is that > we > already solved most of this when we did `var`, with upward projection. > To recap, we spent a lot of time with `var` on what to do about non-denotable > types. These included the null type (banned on the grounds of uselessness), > intersection types (allowed), and capture types (sanitized with upward > projection.) The basic idea of upward projection is that when we infer > List, we replace it with a super type that has no capture types, and get > List out. (There’s also a downward projection.) > Let’s start with your examples of where ordinary inference produces an > undesirable result, and then evaluate whether either or the projections solves > the problem? I don't think current projections are enough because we may want the inference to insert a wildcard by itsef, for example with Object o = ... var list = (List<>) o; or maybe we should not try to infer such code. Rémi
Re: [External] : Re: Diamond in type patterns (was: Reviewing feedback on patterns in switch)
It's more an engineering thing here, we have far more casts than switch + pattern in existing code, and given that we suppose (perhaps wrongly) that the semantics of the inference is not exactly one already existing, I’d like to drill into this supposition. My supposition (maybe wrong) is that we already solved most of this when we did `var`, with upward projection. To recap, we spent a lot of time with `var` on what to do about non-denotable types. These included the null type (banned on the grounds of uselessness), intersection types (allowed), and capture types (sanitized with upward projection.) The basic idea of upward projection is that when we infer List, we replace it with a super type that has no capture types, and get List out. (There’s also a downward projection.) Let’s start with your examples of where ordinary inference produces an undesirable result, and then evaluate whether either or the projections solves the problem?
Re: [External] : Re: Diamond in type patterns (was: Reviewing feedback on patterns in switch)
> From: "Brian Goetz" > To: "Remi Forax" > Cc: "amber-spec-experts" > Sent: Wednesday, January 26, 2022 3:34:21 PM > Subject: Re: [External] : Re: Diamond in type patterns (was: Reviewing > feedback > on patterns in switch) >> I think we should figure out how it should work on cast and then we can >> happily >> applied it on patterns. > I’m happy to have the cast discussion happen concurrently, but right now, my > priority is on patterns, as we’re already two previews into > patterns-in-switch. remember, the move from preview to real feature should be only when we are ready > But I’m not ready to say “we can’t solve this for patterns unless we also > solve > it for cast RIGHT NOW. So I agree with the goal (solve it everywhere, > eventually) but not with the ordering constraint. It's more an engineering thing here, we have far more casts than switch + pattern in existing code, and given that we suppose (perhaps wrongly) that the semantics of the inference is not exactly one already existing, i think we will get better result if we try to automatically transforms all existing casts using a parametrized type to diamond casts and see when the inference fails and why. Also this feature is fully orthogonal with the rest of the patterns because the diamond syntax in type pattern is an invalid syntax, so this feature can have it's own tempo. >> despite the syntax being the same, the diamond syntax, i don't think we can >> reuse the same inference rules between the new diamond and the cast diamond. > Understood. (This is why, for example, we introduced upward and downward > projection when we did var, because the rules for inference were not what we > wanted for var.) But before we go on to the details, are we agreed on the > goal? Agree on the goal, but do you agree on the methodology i propose above. Rémi
Re: [External] : Re: Treatment of total patterns (was: Reviewing feedback on patterns in switch)
> 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: Patterns and GADTs (was: Reviewing feedback on patterns in switch)
> From: "Brian Goetz" > To: "Remi Forax" > Cc: "amber-spec-experts" > Sent: Wednesday, January 26, 2022 3:30:45 PM > Subject: Re: [External] : Re: Patterns and GADTs (was: Reviewing feedback on > patterns in switch) > I don’t object to having something explicit in the code, but I do object to > having that be unchecked. In the original example: >> T unbox(Node n) { >> return switch (n) { >> case A n -> n.t; >> case B n -> n.s; >> }; >> } > we could cast `n,s` to T, but the compiler would have no reason to believe > that > this is valid, so it would give us an unchecked warning. But the reality is, > we > do have enough information to validate this. Now, I’m not sure if users would > be any happier about > case B n -> (T) n.s > even if they did not get an unchecked warning. Still, a cast is probably > better > than new, narrowly targeted syntax here. > If we’re diffident about refining the type of T, we could consider an implicit > conversion (String can be converted to T in a context where we’ve gathered the > appropriate constraints on T), but this is more complicated, and I’m not sure > users will find it any more understandable. Refining the type is something > that > will make more sense to the user (“I know T is String here!”) than complex > rules about when we can funge T and String. I agree, for the compiler, it should be like adding a constraint T = String. The conversion is an equivalent of String <: T which is only half true. Let start without requiring a cast and see if it's too magical or not. Rémi >> On Jan 26, 2022, at 9:16 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: "amber-spec-experts" < [ mailto:amber-spec-experts@openjdk.java.net | >>> amber-spec-experts@openjdk.java.net ] > >>> Sent: Wednesday, January 26, 2022 1:28:19 PM >>> Subject: Re: [External] : Re: Patterns and GADTs (was: Reviewing feedback on >>> patterns in switch) >>>> The instanceof example is not a source backward compatible change, >>>> remember that >>>> instanceof is not a preview feature. >>> I’m well aware, can you give an example where flow typing of *type variables >>> only* might lead to incompatibility? (I’m aware that this is a possibility, >>> but >>> you’re stating it like its a fact already on the table.) For example, where >>> it >>> would change overload selection (this is where flow typing for variables >>> falls >>> down, among other places.) >> sure, >> sealed interface Node { } >> record A(T t) implements Node { } >> record B(String s) implements Node { } >> void foo(Object o) { } >> void foo(List list) { } >> void m() { >> if (node instanceof B b) { >> foo(List.of()); >> } >> } >> Rémi
Re: Reviewing feedback on patterns in switch
To summarize the feedback so far (please, don’t reply here, use the threads): > 1. Treatment of total patterns in switch / instanceof There seems to be overall inclination to adjust semantics of *switch* (not patterns) to be more in line with historical switch behavior. This could be summarized as “if you want null, say null”, at least for switch, which we can justify because switch has a historical hostility to null. The cost is that refactoring / code transform will be slightly less transparent. Would be great to have a catalog of these. > 2. Positioning of guards There were two points here: whether a guard is attached to a pattern or to a case, and secondarily, how we denote a guard. There seems to be overall agreement so far that attaching to the case is preferable; there is not yet agreement on syntax. > 3. Type refinements for GADTs There’s some general feeling that we have a problem that needs to be solved, but some diffidence about refining type variables as a result of pattern matching. > 4. Diamond for type patterns (and record patterns) No one seems to object to the notion of diamond in patterns. Some questions about “why didn’t we do implicit diamond” (as we did with method references.) Some questions about “are there other contexts we should be doing this in." So far we’ve heard from Remi and Tagir. Would like to hear from others.
Re: [External] : Re: Patterns and GADTs (was: Reviewing feedback on patterns in switch)
I don’t object to having something explicit in the code, but I do object to having that be unchecked. In the original example: T unbox(Node n) { return switch (n) { case A n -> n.t; case B n -> n.s; }; } we could cast `n,s` to T, but the compiler would have no reason to believe that this is valid, so it would give us an unchecked warning. But the reality is, we do have enough information to validate this. Now, I’m not sure if users would be any happier about case B n -> (T) n.s even if they did not get an unchecked warning. Still, a cast is probably better than new, narrowly targeted syntax here. If we’re diffident about refining the type of T, we could consider an implicit conversion (String can be converted to T in a context where we’ve gathered the appropriate constraints on T), but this is more complicated, and I’m not sure users will find it any more understandable. Refining the type is something that will make more sense to the user (“I know T is String here!”) than complex rules about when we can funge T and String. On Jan 26, 2022, at 9:16 AM, fo...@univ-mlv.fr<mailto:fo...@univ-mlv.fr> wrote: From: "Brian Goetz" mailto:brian.go...@oracle.com>> To: "Remi Forax" mailto:fo...@univ-mlv.fr>> Cc: "amber-spec-experts" mailto:amber-spec-experts@openjdk.java.net>> Sent: Wednesday, January 26, 2022 1:28:19 PM Subject: Re: [External] : Re: Patterns and GADTs (was: Reviewing feedback on patterns in switch) The instanceof example is not a source backward compatible change, remember that instanceof is not a preview feature. I’m well aware, can you give an example where flow typing of *type variables only* might lead to incompatibility? (I’m aware that this is a possibility, but you’re stating it like its a fact already on the table.) For example, where it would change overload selection (this is where flow typing for variables falls down, among other places.) sure, sealed interface Node { } record A(T t) implements Node { } record B(String s) implements Node { } void foo(Object o) { } void foo(List list) { } void m() { if (node instanceof B b) { foo(List.of()); } } Rémi
Re: [External] : Re: Diamond in type patterns (was: Reviewing feedback on patterns in switch)
I think we should figure out how it should work on cast and then we can happily applied it on patterns. I’m happy to have the cast discussion happen concurrently, but right now, my priority is on patterns, as we’re already two previews into patterns-in-switch. But I’m not ready to say “we can’t solve this for patterns unless we also solve it for cast RIGHT NOW. So I agree with the goal (solve it everywhere, eventually) but not with the ordering constraint. despite the syntax being the same, the diamond syntax, i don't think we can reuse the same inference rules between the new diamond and the cast diamond. Understood. (This is why, for example, we introduced upward and downward projection when we did var, because the rules for inference were not what we wanted for var.) But before we go on to the details, are we agreed on the goal?
Re: [External] : Re: Diamond in type patterns (was: Reviewing feedback on patterns in switch)
> From: "Brian Goetz" > To: "Remi Forax" > Cc: "amber-spec-experts" > Sent: Wednesday, January 26, 2022 1:23:04 PM > Subject: Re: [External] : Re: Diamond in type patterns (was: Reviewing > feedback > on patterns in switch) >> The questions we did not answer the last time we talk about that subject >> - why should we allow raw types here ? > We already have a clear precedent with type patterns in instanceof — which is > not a preview feature any more. So for one, now you’re talking about making a > *change* to the existing language semantics. There are other concerns too. >> - given that this is equivalent to an instanceof + cast, why we can not use >> diamond inference on cast ? > You’re not being clear about what you’re saying, you could be saying either of > the following (or others): > - You’re proposing diamond here, but not there, then your proposal is > inconsistent, and therefore stupid. > - I love your proposal, but I think we should additionally talk about other > places to use diamond as well. > I can’t tell which of these you’re saying, or maybe its something else? I think we should figure out how it should work on cast and then we can happily applied it on patterns. >> - how this inference work ? Is is the same inference than with the diamond >> constructor ? > Again, I can’t tell whether you’re saying “this is dumb, it can’t work”, or > “this is great, but I can’t figure out the details.” The rules for what you can use as argument of a type parameter when doing a new and when doing a cast are not the same, for examples, new ArrayList() is not a valid code while (ArrayList) is a perfect valid cast, new ArrayList[3] is not a valid code while (ArrayList[]) may or may not be a valid cast, new ArrayList() is a valid code while (ArrayList) may or may not be a valid cast . despite the syntax being the same, the diamond syntax, i don't think we can reuse the same inference rules between the new diamond and the cast diamond. regards, Rémi
Re: [External] : Re: Patterns and GADTs (was: Reviewing feedback on patterns in switch)
> From: "Brian Goetz" > To: "Remi Forax" > Cc: "amber-spec-experts" > Sent: Wednesday, January 26, 2022 1:28:19 PM > Subject: Re: [External] : Re: Patterns and GADTs (was: Reviewing feedback on > patterns in switch) >> The instanceof example is not a source backward compatible change, remember >> that >> instanceof is not a preview feature. > I’m well aware, can you give an example where flow typing of *type variables > only* might lead to incompatibility? (I’m aware that this is a possibility, > but > you’re stating it like its a fact already on the table.) For example, where it > would change overload selection (this is where flow typing for variables falls > down, among other places.) sure, sealed interface Node { } record A(T t) implements Node { } record B(String s) implements Node { } void foo(Object o) { } void foo(List list) { } void m() { if (node instanceof B b) { foo(List.of()); } } Rémi
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. On Jan 26, 2022, at 8:45 AM, fo...@univ-mlv.fr<mailto: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)
> 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)
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)
> 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: [External] : Re: Positioning of guards (was: Reviewing feedback on patterns in switch)
> For the record: I like the current version with &&. It's short and > easy to understand (as people already know what && means in Java). I > see no reason in replacing it with `when`, which is more limiting. I like the && syntax too (though we can invent some nasty puzzlers with booleans, such as `case false && false`, which are not so likable.) But the two are not uncoupled; its harder to imagine && as part of the case, and not as part of the pattern. We can of course do many things here, but I worry that the choice of operator affects peoples intuition about the semantics. (Historically keyword operators like instanceof have the weakest binding precedence.)
Re: [External] : Re: Patterns and GADTs (was: Reviewing feedback on patterns in switch)
The instanceof example is not a source backward compatible change, remember that instanceof is not a preview feature. I’m well aware, can you give an example where flow typing of *type variables only* might lead to incompatibility? (I’m aware that this is a possibility, but you’re stating it like its a fact already on the table.) For example, where it would change overload selection (this is where flow typing for variables falls down, among other places.) The main objection to that is that we do not have flow scoping for local variables but we have it for type variables which is weird. We have inference for type variables when we don’t have them for types too. There are s many differences between type variables and ordinary type uses. But yes, this would be flow typing for type variables. I wonder if we can come with an explicit syntax for it, the same way instanceof String s is an explicit syntax for local variables. By example, something like return switch (n) { case A n -> n.t; case B n -> n.s as T=String; }; but maybe it's too much ceremony. Given the examples, I think this is something that stays better in the background.
Re: [External] : Re: Diamond in type patterns (was: Reviewing feedback on patterns in switch)
The questions we did not answer the last time we talk about that subject - why should we allow raw types here ? We already have a clear precedent with type patterns in instanceof — which is not a preview feature any more. So for one, now you’re talking about making a *change* to the existing language semantics. There are other concerns too. - given that this is equivalent to an instanceof + cast, why we can not use diamond inference on cast ? You’re not being clear about what you’re saying, you could be saying either of the following (or others): - You’re proposing diamond here, but not there, then your proposal is inconsistent, and therefore stupid. - I love your proposal, but I think we should additionally talk about other places to use diamond as well. I can’t tell which of these you’re saying, or maybe its something else? - how this inference work ? Is is the same inference than with the diamond constructor ? Again, I can’t tell whether you’re saying “this is dumb, it can’t work”, or “this is great, but I can’t figure out the details.”
Re: Positioning of guards (was: Reviewing feedback on patterns in switch)
- Original Message - > From: "Tagir Valeev" > To: "Brian Goetz" > Cc: "amber-spec-experts" > Sent: Wednesday, January 26, 2022 4:50:05 AM > Subject: Re: Positioning of guards (was: Reviewing feedback on patterns in > switch) > Hello! > > For the record: I like the current version with &&. It's short and > easy to understand (as people already know what && means in Java). I > see no reason in replacing it with `when`, which is more limiting. > >> because of the potential confusion should we ever choose to support switch >> over >> boolean > > It looks like any boolean expression that is a potentially constant > differs syntactically from the guarded pattern, so we can distinguish > between guarded pattern with && and boolean expression on AST level > without resolving the references. End users will unlikely use anything > other than explicit 'true' and 'false' constants, so it will add some > complexity to the compiler but does not add any problems to real users > >> because the && did not stand out enough as turning a total pattern into a >> partial one > > I think it's a matter of taste and habit. I, for one, already get used > to it. It signals about partiality much more, compared to a simple > type pattern. Looking at `CharSequence cs`, you cannot say whether > it's total or not if you don't know the type of the selector > expression. However, looking at `CharSequence cs && cs.length() > 0` > you are completely sure it's not total. So if we need a clear signal > to tell total and partial patterns apart, we have much bigger problems > with type patterns. > >> Guarded patterns are never total > Except when guard is a constant expression that evaluates to `true`: > > void test(Object obj) { >switch (obj) { // compiles >case Object s && true -> System.out.println(s); >} > } I think we should separate the two ideas in Brian's mail, one is should we allow a guard inside a pattern ? and the other is what is the syntax for a guard ? My position is that we should only allow guard in a switch, not as pattern. And i see no problem to use "&&" instead of "when", as Tagir, i'm kind of used to it too. regards, Rémi > > On Wed, Jan 26, 2022 at 2:49 AM Brian Goetz wrote: >> >> > 2. Positioning of guards >> >> We received several forms of feedback over the form and placement of guarded >> patterns. Recall that we define a guarded pattern to be `P && g`, where P >> is a >> pattern and g is a boolean expression. Guarded patterns are never total. >> Note >> that we had a choice of the guard being part of the pattern, or being part of >> the `case` label; the current status chooses the former. (Part of our >> reasoning was that there might be other partial pattern contexts coming, and >> we >> didn’t want to solve this problem each time. (For instanceof, it makes no >> difference.) ) >> >> I am prepared to reconsider the association of the guard with the pattern, >> and >> instead treat it as part of the case. This is expressively weaker but may >> have >> other advantages. >> >> Additionally, people objected to the use of &&, not necessarily because >> “keywords are better”, but because of the potential confusion should we ever >> choose to support switch over boolean, and because the && did not stand out >> enough as turning a total pattern into a partial one. What the alternative >> looks like is something like: >> >> switch (x) { >> case Foo(var x, var y) >> when x == y -> A; >> case Foo(var x, var y) -> B; >> } >> >> Here, `when` (bike shed to be painted separately) is a qualifier on the case, >> not the pattern. A total pattern with a `when` is considered a partial case. >> This simplifies patterns, and moves the complexity of guards into switch, >> where arguably it belongs. >> >> The loss of expressiveness is in not allowing nested patterns like: >> >> P(Q && guard) >> >> and instead having to move the guard to after the matching construct. Some >> users recoiled at seeing guards inside pattern invocations; it seemed to some >> like mixing two things that should stay separate. (For unrolling a nested >> pattern, `case P(Q)` where Q is not total unrolls to `case Pvar alpha) when > > alpha instanceof Q`.)
Re: Treatment of total patterns (was: Reviewing feedback on patterns in switch)
- 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 >> tha
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. 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: Positioning of guards (was: Reviewing feedback on patterns in switch)
Hello! For the record: I like the current version with &&. It's short and easy to understand (as people already know what && means in Java). I see no reason in replacing it with `when`, which is more limiting. > because of the potential confusion should we ever choose to support switch > over boolean It looks like any boolean expression that is a potentially constant differs syntactically from the guarded pattern, so we can distinguish between guarded pattern with && and boolean expression on AST level without resolving the references. End users will unlikely use anything other than explicit 'true' and 'false' constants, so it will add some complexity to the compiler but does not add any problems to real users > because the && did not stand out enough as turning a total pattern into a > partial one I think it's a matter of taste and habit. I, for one, already get used to it. It signals about partiality much more, compared to a simple type pattern. Looking at `CharSequence cs`, you cannot say whether it's total or not if you don't know the type of the selector expression. However, looking at `CharSequence cs && cs.length() > 0` you are completely sure it's not total. So if we need a clear signal to tell total and partial patterns apart, we have much bigger problems with type patterns. > Guarded patterns are never total Except when guard is a constant expression that evaluates to `true`: void test(Object obj) { switch (obj) { // compiles case Object s && true -> System.out.println(s); } } On Wed, Jan 26, 2022 at 2:49 AM Brian Goetz wrote: > > > 2. Positioning of guards > > We received several forms of feedback over the form and placement of guarded > patterns. Recall that we define a guarded pattern to be `P && g`, where P is > a pattern and g is a boolean expression. Guarded patterns are never total. > Note that we had a choice of the guard being part of the pattern, or being > part of the `case` label; the current status chooses the former. (Part of > our reasoning was that there might be other partial pattern contexts coming, > and we didn’t want to solve this problem each time. (For intsanceof, it makes > no difference.) ) > > I am prepared to reconsider the association of the guard with the pattern, > and instead treat it as part of the case. This is expressively weaker but > may have other advantages. > > Additionally, people objected to the use of &&, not necessarily because > “keywords are better”, but because of the potential confusion should we ever > choose to support switch over boolean, and because the && did not stand out > enough as turning a total pattern into a partial one. What the alternative > looks like is something like: > > switch (x) { > case Foo(var x, var y) > when x == y -> A; > case Foo(var x, var y) -> B; > } > > Here, `when` (bike shed to be painted separately) is a qualifier on the case, > not the pattern. A total pattern with a `when` is considered a partial case. > This simplifies patterns, and moves the complexity of guards into switch, > where arguably it belongs. > > The loss of expressiveness is in not allowing nested patterns like: > > P(Q && guard) > > and instead having to move the guard to after the matching construct. Some > users recoiled at seeing guards inside pattern invocations; it seemed to some > like mixing two things that should stay separate. (For unrolling a nested > pattern, `case P(Q)` where Q is not total unrolls to `case Pvar alpha) when > alpha instanceof Q`.)
Re: Treatment of total patterns (was: Reviewing feedback on patterns in switch)
> 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
Re: Positioning of guards (was: Reviewing feedback on patterns in switch)
- Original Message - > From: "Brian Goetz" > To: "amber-spec-experts" > Sent: Tuesday, January 25, 2022 8:49:02 PM > Subject: Positioning of guards (was: Reviewing feedback on patterns in switch) >> 2. Positioning of guards > > We received several forms of feedback over the form and placement of guarded > patterns. Recall that we define a guarded pattern to be `P && g`, where P is > a > pattern and g is a boolean expression. Guarded patterns are never total. > Note > that we had a choice of the guard being part of the pattern, or being part of > the `case` label; the current status chooses the former. (Part of our > reasoning was that there might be other partial pattern contexts coming, and > we > didn’t want to solve this problem each time. (For intsanceof, it makes no > difference.) ) > > I am prepared to reconsider the association of the guard with the pattern, and > instead treat it as part of the case. This is expressively weaker but may > have > other advantages. > > Additionally, people objected to the use of &&, not necessarily because > “keywords are better”, but because of the potential confusion should we ever > choose to support switch over boolean, and because the && did not stand out > enough as turning a total pattern into a partial one. What the alternative > looks like is something like: > >switch (x) { >case Foo(var x, var y) >when x == y -> A; >case Foo(var x, var y) -> B; >} > > Here, `when` (bike shed to be painted separately) is a qualifier on the case, > not the pattern. A total pattern with a `when` is considered a partial case. > This simplifies patterns, and moves the complexity of guards into switch, > where arguably it belongs. > > The loss of expressiveness is in not allowing nested patterns like: > >P(Q && guard) > > and instead having to move the guard to after the matching construct. Some > users recoiled at seeing guards inside pattern invocations; it seemed to some > like mixing two things that should stay separate. (For unrolling a nested > pattern, `case P(Q)` where Q is not total unrolls to `case Pvar alpha) when > alpha instanceof Q`.) I think it's a good simplification. Rémi
Re: Patterns and GADTs (was: Reviewing feedback on patterns in switch)
- Original Message - > From: "Brian Goetz" > To: "amber-spec-experts" > Sent: Tuesday, January 25, 2022 8:49:08 PM > Subject: Patterns and GADTs (was: Reviewing feedback on patterns in switch) >> 3. Type refinements for GADTs > > There are a number of unsatisfying aspects to how we currently handle GADTs; > specifically, we are missing the type refinement process outlined in "Simple > unification-based type inference for GADTs” (SPJ et al, 2005). Here are some > examples of where we fall down. > > Suppose we have > >sealed interface Node { } >record A(T t) extends Node { } >record B(String s) extends Node { } > > and we want to write: > > T unbox(Node n) { >return switch (n) { >case A n -> n.t; >case B n -> n.s; >}; >} > > The RHS of all the arrows must be compatible with the return type, which is T. > Clearly that is true for the first case, but not for the second; the compiler > doesn’t know that T has to be String if we’ve matched a Node to B. What is > missing is to refine the type of T based on matches. Here, we would gather an > additional constraint on `case B` for T=String; if we had a case which was > covariant in T: > >record C(T t) > > then a `case C` would gather an additional constraint of T <: B for its > RHS. > > More generally, any code dominated by a match that provides additional bounds > for type variables could benefit from those bounds. For example, we’d > probably > want the same in: > >if (n instanceof B b) { /* T is String here */ } > > and (as with flow scoping): > >if (! (n instanceof B b)) >throw … >// T is String here > > We can probably piggy back the specification of this on flow scoping, > appealing > to “wherever a binding introduced by this pattern would be in scope.” I agree that we should do something to support GADTs The instanceof example is not a source backward compatible change, remember that instanceof is not a preview feature. The main objection to that is that we do not have flow scoping for local variables but we have it for type variables which is weird. I wonder if we can come with an explicit syntax for it, the same way instanceof String s is an explicit syntax for local variables. By example, something like return switch (n) { case A n -> n.t; case B n -> n.s as T=String; }; but maybe it's too much ceremony. regards, Rémi
Re: Diamond in type patterns (was: Reviewing feedback on patterns in switch)
- Original Message - > From: "Brian Goetz" > To: "amber-spec-experts" > Sent: Tuesday, January 25, 2022 8:49:12 PM > Subject: Diamond in type patterns (was: Reviewing feedback on patterns in > switch) >> 4. Diamond for type patterns (and record patterns) > > > The type pattern `T t` declares `t`, if the pattern matches, with the type T. > If T is a generic type, then we do a consistency check to ensure soundness: > >List list = … >switch (list) { >case ArrayList a: A // ok >case ArrayList a: B // ok >case ArrayList a: C// ok, raw type >case ArrayList a: // error, would require unchecked conversion > } > > All of these make sense, but users are going to be tempted to use `case > ArrayList a` rather than the full `case ArrayList a`, and then be sad > (or confused) when they get type errors. Since the type can be precisely > defined by inference, this seems a place for allowing diamond: > >case ArrayList<> a: B > > (And the same when we have record patterns.) The questions we did not answer the last time we talk about that subject - why should we allow raw types here ? - given that this is equivalent to an instanceof + cast, why we can not use diamond inference on cast ? - how this inference work ? Is is the same inference than with the diamond constructor ? By example, if we have List list = ... switch(list) { case ArrayList<> a: } Do we really want to infer ArrayList to then rejects it because it's an unsafe cast. regards, Rémi
Diamond in type patterns (was: Reviewing feedback on patterns in switch)
> 4. Diamond for type patterns (and record patterns) The type pattern `T t` declares `t`, if the pattern matches, with the type T. If T is a generic type, then we do a consistency check to ensure soundness: List list = … switch (list) { case ArrayList a: A // ok case ArrayList a: B // ok case ArrayList a: C// ok, raw type case ArrayList a: // error, would require unchecked conversion } All of these make sense, but users are going to be tempted to use `case AerrayList a` rather than the full `case ArrayList a`, and then be sad (or confused) when they get type errors. Since the type can be precisely defined by inference, this seems a place for allowing diamond: case ArrayList<> a: B (And the same when we have record patterns.)
Patterns and GADTs (was: Reviewing feedback on patterns in switch)
> 3. Type refinements for GADTs There are a number of unsatisfying aspects to how we currently handle GADTs; specifically, we are missing the type refinement process outlined in "Simple unification-based type inference for GADTs” (SPJ et al, 2005). Here are some examples of where we fall down. Suppose we have sealed interface Node { } record A(T t) extends Node { } record B(String s) extends Node { } and we want to write: T unbox(Node n) { return switch (n) { case A n -> n.t; case B n -> n.s; } } The RHS of all the arrows must be compatible with the return type, which is T. Clearly that is true for the first case, but not for the second; the compiler doesn’t know that T has to be String if we’ve matched a Node to B. What is missing is to refine the type of T based on matches. Here, we would gather an additional constraint on `case B` for T=String; if we had a case which was covariant in T: record C(T t) then a `case C` would gather an additional constraint of T <: B for its RHS. More generally, any code dominated by a match that provides additional bounds for type variables could benefit from those bounds. For example, we’d probably want the same in: if (n instanceof B b) { /* T is String here */ } and (as with flow scoping): if (! (n instanceof B b)) throw … // T is String here We can probably piggy back the specification of this on flow scoping, appealing to “wherever a binding introduced by this pattern would be in scope.”
Positioning of guards (was: Reviewing feedback on patterns in switch)
> 2. Positioning of guards We received several forms of feedback over the form and placement of guarded patterns. Recall that we define a guarded pattern to be `P && g`, where P is a pattern and g is a boolean expression. Guarded patterns are never total. Note that we had a choice of the guard being part of the pattern, or being part of the `case` label; the current status chooses the former. (Part of our reasoning was that there might be other partial pattern contexts coming, and we didn’t want to solve this problem each time. (For intsanceof, it makes no difference.) ) I am prepared to reconsider the association of the guard with the pattern, and instead treat it as part of the case. This is expressively weaker but may have other advantages. Additionally, people objected to the use of &&, not necessarily because “keywords are better”, but because of the potential confusion should we ever choose to support switch over boolean, and because the && did not stand out enough as turning a total pattern into a partial one. What the alternative looks like is something like: switch (x) { case Foo(var x, var y) when x == y -> A; case Foo(var x, var y) -> B; } Here, `when` (bike shed to be painted separately) is a qualifier on the case, not the pattern. A total pattern with a `when` is considered a partial case. This simplifies patterns, and moves the complexity of guards into switch, where arguably it belongs. The loss of expressiveness is in not allowing nested patterns like: P(Q && guard) and instead having to move the guard to after the matching construct. Some users recoiled at seeing guards inside pattern invocations; it seemed to some like mixing two things that should stay separate. (For unrolling a nested pattern, `case P(Q)` where Q is not total unrolls to `case Pvar alpha) when alpha instanceof Q`.)
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.
Reviewing feedback on patterns in switch
We’ve previewed patterns in switch for two rounds, and have received some feedback. Overall, things work quite well, but there were a few items which received some nontrivial feedback, and I’m prepared to suggest some changes based on them. I’ll summarize them here and create a new thread for each with a more detailed description. I’ll make a call for additional items a little later; for now, let’s focus on these items before adding new things (or reopening old ones.) 1. Treatment of total patterns in switch / instanceof 2. Positioning of guards 3. Type refinements for GADTs 4. Diamond for type patterns (and record patterns)