From amaembo at gmail.com Thu Feb 3 05:41:53 2022 From: amaembo at gmail.com (Tagir Valeev) Date: Thu, 3 Feb 2022 12:41:53 +0700 Subject: Control flow analysis for exhaustive pattern-switch statement Message-ID: Hello! JLS requires (14.11.2) that the new "enhanced" switch statement (not expressions) must be exhaustive. In particular, for the switch over sealed abstract type, it's required to list all the permitted subtypes or provide a total/default branch. This is good. However, this means that from CFG point of view exactly one switch branch must always be visited. This is indeed so if we take a look at the bytecode: the synthetic default branch throws IncompatibleClassChangeError(). But compiler ignores this fact and refuses to compile the code like this (tried 18-ea+33-2077): sealed interface Parent {} record A() implements Parent {} record B() implements Parent {} void test(Parent p) { int x; switch (p) { case A a -> x = 1; case B b -> x = 2; } System.out.println(x); // error: variable x might not have been initialized } I think this is a mistake that should be corrected: in this code, `x` should be considered as definitely assigned. I understand that the same reasoning does not apply for switch over enums, as for compatibility reasons, default behavior is to do nothing. However, for patterns, uninitialized `x` cannot appear after the switch, even if we recompile the sealed interface separately adding one more inheritor. I try to understand what's written in 16.2.9 regarding this [1]. Unfortunately, its current state looks confusing to me. Sorry if I'm missing something, as I'm not very experienced in reading chapter 16 of JLS. Nevertheless, it says: V is [un]assigned after a switch statement (14.11) iff all of the following are true: ... Original spec: - If there is no default label in the switch block, or if the switch block ends with a switch label followed by the } separator, then V is [un]assigned after the selector expression. Preview spec: - If the switch block covers the type of the selector expression, or if the switch block ends with a switch label followed by the } separator, then V is [un]assigned after the selector expression. It looks strange that "no default label" (~= non-exhaustive) was replaced with "covers the type" (= exhaustive). Was negation lost somewhere? In current state it looks like, all exhaustive switches (which is almost all switches), including ones with `default` branch cannot definitely assign a variable, which contradicts the previous state. If negation is actually lost, then the sample above should compile, as it's exhaustive. Sorry if this was already discussed. With best regards, Tagir Valeev [1] https://cr.openjdk.java.net/~gbierman/jep406/jep406-20210608/specs/patterns-switch-jls.html#jls-16.2.9 From brian.goetz at oracle.com Thu Feb 3 16:35:13 2022 From: brian.goetz at oracle.com (Brian Goetz) Date: Thu, 3 Feb 2022 11:35:13 -0500 Subject: Control flow analysis for exhaustive pattern-switch statement In-Reply-To: References: Message-ID: Thanks Tagir for digging into this.? This is good analysis. The decision to require completeness of enhanced switches was taken relatively late, so its quite possible we have left something out.? And in the next round, when nested patterns come along, we need to formalize totality and remainder, which also intersects with this problem.? So this is timely. On 2/3/2022 12:41 AM, Tagir Valeev wrote: > Hello! > > JLS requires (14.11.2) that the new "enhanced" switch statement (not > expressions) must be exhaustive. In particular, for the switch over > sealed abstract type, it's required to list all the permitted subtypes > or provide a total/default branch. This is good. However, this means > that from CFG point of view exactly one switch branch must always be > visited. This is indeed so if we take a look at the bytecode: the > synthetic default branch throws IncompatibleClassChangeError(). But > compiler ignores this fact and refuses to compile the code like this > (tried 18-ea+33-2077): > > sealed interface Parent {} > record A() implements Parent {} > record B() implements Parent {} > > void test(Parent p) { > int x; > switch (p) { > case A a -> x = 1; > case B b -> x = 2; > } > System.out.println(x); // error: variable x might not have been initialized > } > > I think this is a mistake that should be corrected: in this code, `x` > should be considered as definitely assigned. Agreed.? This is exactly why we have "remainder"; the set { A, B } is total on Parent, with remainder; the remainder includes all novel subclasses of Parent that were born after test(Parent) was compiled.? Our goal is that for a switch (or other pattern match construct) where the pattern(s) are total on the target, is that you cannot arrive at the statement after the switch without one of the branches being taken.? In the case of the remainder (e.g., a late-breaking C class), the switch will complete abruptly, so the correct characterization of your "exactly one" is that "if the switch completes normally, exactly one branch was visited."? Then we arrange for the switch to complete abruptly in case of remainder. (Remainder also includes things like Box(null) in a case Box(Bag(String s)); we had a long thread about the interesting shape of remainder sets last summer, and this topic will soon return, hopefully with the aid of more mathematical formalism.) > I understand that the same reasoning does not apply for switch over > enums, as for compatibility reasons, default behavior is to do > nothing. However, for patterns, uninitialized `x` cannot appear after > the switch, even if we recompile the sealed interface separately > adding one more inheritor. Slight correction: *statement* switches over enums.? We've carved out a place where the only switches that are not total, are *statement* switches over the legacy types with the legacy switch labels.? In that case, DU analysis picks up some of the slack. (It is still on our "to be considered" list whether it is worth it to allow statement switches to be explicitly marked as total to engage greater typechecking, or whether we want to embark on the decade-long path of warning increasingly loudly on non-total switches until we eventually make them illegal.) > I try to understand what's written in 16.2.9 regarding this [1]. > Unfortunately, its current state looks confusing to me. Sorry if I'm > missing something, as I'm not very experienced in reading chapter 16 > of JLS. Nevertheless, it says: > > V is [un]assigned after a switch statement (14.11) iff all of the > following are true: > ... > Original spec: > - If there is no default label in the switch block, or if the switch > block ends with a switch label followed by the } separator, then V is > [un]assigned after the selector expression. > Preview spec: > - If the switch block covers the type of the selector expression, or > if the switch block ends with a switch label followed by the } > separator, then V is [un]assigned after the selector expression. > > It looks strange that "no default label" (~= non-exhaustive) was > replaced with "covers the type" (= exhaustive). Was negation lost > somewhere? In current state it looks like, all exhaustive switches > (which is almost all switches), including ones with `default` branch > cannot definitely assign a variable, which contradicts the previous > state. > > If negation is actually lost, then the sample above should compile, as > it's exhaustive. I'll take a read-through of the spec (or more likely, Gavin will beat me to it) and respond more completely at that point. Thanks, -Brian -------------- next part -------------- An HTML attachment was scrubbed... URL: From forax at univ-mlv.fr Fri Feb 4 08:48:36 2022 From: forax at univ-mlv.fr (Remi Forax) Date: Fri, 4 Feb 2022 09:48:36 +0100 (CET) Subject: Control flow analysis for exhaustive pattern-switch statement In-Reply-To: References: Message-ID: <821358410.9936367.1643964516095.JavaMail.zimbra@u-pem.fr> > From: "Brian Goetz" > To: "Tagir Valeev" , "amber-spec-experts" > > Sent: Thursday, February 3, 2022 5:35:13 PM > Subject: Re: Control flow analysis for exhaustive pattern-switch statement > Thanks Tagir for digging into this. This is good analysis. > The decision to require completeness of enhanced switches was taken relatively > late, so its quite possible we have left something out. And in the next round, > when nested patterns come along, we need to formalize totality and remainder, > which also intersects with this problem. So this is timely. [...] >> I understand that the same reasoning does not apply for switch over >> enums, as for compatibility reasons, default behavior is to do >> nothing. However, for patterns, uninitialized `x` cannot appear after >> the switch, even if we recompile the sealed interface separately >> adding one more inheritor. > Slight correction: *statement* switches over enums. We've carved out a place > where the only switches that are not total, are *statement* switches over the > legacy types with the legacy switch labels. In that case, DU analysis picks up > some of the slack. > (It is still on our "to be considered" list whether it is worth it to allow > statement switches to be explicitly marked as total to engage greater > typechecking, or whether we want to embark on the decade-long path of warning > increasingly loudly on non-total switches until we eventually make them > illegal.) Both options are not exclusive, we can have a switch marked as exhaustive AND have a warning to nudge people to the "right" path. I believe we should mark a way to mark switch as total but i would prefer to have a syntax which is not something like total-switch which to me is a regression given that we have decided to rehabilitate the switch instead of using another keyword (what's Brian called the snitch approach). If we try to summarize the approachs we have discussed, we have - use a new keyword like "total-switch" - use a new keyword inside the parenthesis of the switch, switch(total value) { ... } - use default -> throw new AnException() - use default -> throw; as a shortcut The two later propositions are not good because adding a "default" make the switch exhaustive but we are lose the typechecking when a new enum constant is introduced. The two former propositions are not good because it's a snitch in disguise. I believe a good compromise might be to have a syntax inside the curly braces like "default -> " but not using "default" because it's not the semantics we want. It just occurs to me that we can use an ellipsis (...) for that switch(option) { case READ -> { } case WRITE -> { } case READ_WRITE -> { } ... // this is a real syntax, it means that the switch is total } An ellipsis is an opt-in way to ask for an exhaustive checking, it will fail at compile time if one of the constants is not listed, and in case of separate compilation at runtime an error will be thrown. [...] > Thanks, > -Brian R?mi -------------- next part -------------- An HTML attachment was scrubbed... URL: From gavin.bierman at oracle.com Mon Feb 7 16:31:17 2022 From: gavin.bierman at oracle.com (Gavin Bierman) Date: Mon, 7 Feb 2022 16:31:17 +0000 Subject: Control flow analysis for exhaustive pattern-switch statement In-Reply-To: References: Message-ID: <90EEDD49-6054-48B0-9824-9CA62467A0A6@oracle.com> Thanks Tagir. I?ll take a look at the spec issue and report back. Gavin > On 3 Feb 2022, at 05:41, Tagir Valeev wrote: > > Hello! > > JLS requires (14.11.2) that the new "enhanced" switch statement (not > expressions) must be exhaustive. In particular, for the switch over > sealed abstract type, it's required to list all the permitted subtypes > or provide a total/default branch. This is good. However, this means > that from CFG point of view exactly one switch branch must always be > visited. This is indeed so if we take a look at the bytecode: the > synthetic default branch throws IncompatibleClassChangeError(). But > compiler ignores this fact and refuses to compile the code like this > (tried 18-ea+33-2077): > > sealed interface Parent {} > record A() implements Parent {} > record B() implements Parent {} > > void test(Parent p) { > int x; > switch (p) { > case A a -> x = 1; > case B b -> x = 2; > } > System.out.println(x); // error: variable x might not have been initialized > } > > I think this is a mistake that should be corrected: in this code, `x` > should be considered as definitely assigned. > > I understand that the same reasoning does not apply for switch over > enums, as for compatibility reasons, default behavior is to do > nothing. However, for patterns, uninitialized `x` cannot appear after > the switch, even if we recompile the sealed interface separately > adding one more inheritor. > > I try to understand what's written in 16.2.9 regarding this [1]. > Unfortunately, its current state looks confusing to me. Sorry if I'm > missing something, as I'm not very experienced in reading chapter 16 > of JLS. Nevertheless, it says: > > V is [un]assigned after a switch statement (14.11) iff all of the > following are true: > ... > Original spec: > - If there is no default label in the switch block, or if the switch > block ends with a switch label followed by the } separator, then V is > [un]assigned after the selector expression. > Preview spec: > - If the switch block covers the type of the selector expression, or > if the switch block ends with a switch label followed by the } > separator, then V is [un]assigned after the selector expression. > > It looks strange that "no default label" (~= non-exhaustive) was > replaced with "covers the type" (= exhaustive). Was negation lost > somewhere? In current state it looks like, all exhaustive switches > (which is almost all switches), including ones with `default` branch > cannot definitely assign a variable, which contradicts the previous > state. > > If negation is actually lost, then the sample above should compile, as > it's exhaustive. > > Sorry if this was already discussed. > > With best regards, > Tagir Valeev > > [1] https://cr.openjdk.java.net/~gbierman/jep406/jep406-20210608/specs/patterns-switch-jls.html#jls-16.2.9 From gavin.bierman at oracle.com Wed Feb 9 11:59:02 2022 From: gavin.bierman at oracle.com (Gavin Bierman) Date: Wed, 9 Feb 2022 11:59:02 +0000 Subject: JEP 405 update Message-ID: Dear experts, Just to let you know that I have updated JEP 405: https://openjdk.java.net/jeps/405 You will see that we have removed the array patterns from this JEP (and it has been retitled accordingly). We're still committed to supporting a direct pattern form for arrays, but given our tight schedule and a number of queries about the exact syntactic form for array patterns, we think we'll be better off decoupling them from JEP 405, and releasing them in a future patterns JEP. Comments welcomed! Gavin From forax at univ-mlv.fr Wed Feb 9 16:40:08 2022 From: forax at univ-mlv.fr (Remi Forax) Date: Wed, 9 Feb 2022 17:40:08 +0100 (CET) Subject: JEP 405 update In-Reply-To: References: Message-ID: <68568565.1120088.1644424808438.JavaMail.zimbra@u-pem.fr> Hi Gavin, I don't buy the argument that record patterns promote a null-safe style of programming as this is stated several times in the JEP. The ""null-safety"" (notice the air quotes) mostly comes from the instanceof or the switch (which semantics is equivalent of a cascade of if instanceof), not from the record pattern by itself. You can argue that when a record pattern is nested a nullcheck appears, but it's more than the underlying semantics is a degenerated instanceof when the declared type and the instanceof type are the same. The record pattern is about destructuring after the instanceof/nullcheck has been done, so i find that argument counter productive because it does not help to understand the semantics. Also, we have talked several times to introduce the record pattern when doing an assignment Point point = ... Point(int x, int y) = point; // can use x and y here ! This will throw a NPE if point is null, similarly to an unboxing operation. The null-safety is not attached to the record pattern per se but by the container that use it (instanceof, case of a switch, enclosing pattern). regards, R?mi ----- Original Message ----- > From: "Gavin Bierman" > To: "amber-spec-experts" > Sent: Wednesday, February 9, 2022 12:59:02 PM > Subject: JEP 405 update > Dear experts, > > Just to let you know that I have updated JEP 405: > > https://openjdk.java.net/jeps/405 > > You will see that we have removed the array patterns from this JEP (and it has > been retitled accordingly). We're still committed to supporting a direct pattern > form for arrays, but given our tight schedule and a number of queries about the > exact syntactic form for array patterns, we think we'll be better off decoupling > them from JEP 405, and releasing them in a future patterns JEP. > > Comments welcomed! > Gavin From gavin.bierman at oracle.com Wed Feb 9 22:11:56 2022 From: gavin.bierman at oracle.com (Gavin Bierman) Date: Wed, 9 Feb 2022 22:11:56 +0000 Subject: [External] : Re: JEP 405 update In-Reply-To: <68568565.1120088.1644424808438.JavaMail.zimbra@u-pem.fr> References: <68568565.1120088.1644424808438.JavaMail.zimbra@u-pem.fr> Message-ID: <4B9F216A-CF2A-467F-A4ED-596EC39220F3@oracle.com> Thanks Remi. Yes, this text could be better (interestingly, it is unchanged from nearly a year ago!). I think you?d agree that *pattern matching* with nested patterns provides a null-safe approach to data access, right? That?s really what I am getting at. In that sense, record patterns are a key, because they support nesting of patterns. You are correct that the construct within which we surface pattern matching can have a say in the top-level treatment of null. Indeed, one of the things we are discussing at the moment is exactly whether switch should have a stronger opinion about top-level null! Let me see if I can work on the text some more. Thanks for the feedback, Gavin > On 9 Feb 2022, at 16:40, Remi Forax wrote: > > Hi Gavin, > I don't buy the argument that record patterns promote a null-safe style of programming as this is stated several times in the JEP. > > The ""null-safety"" (notice the air quotes) mostly comes from the instanceof or the switch (which semantics is equivalent of a cascade of if instanceof), not from the record pattern by itself. > > You can argue that when a record pattern is nested a nullcheck appears, but it's more than the underlying semantics is a degenerated instanceof when the declared type and the instanceof type are the same. > > The record pattern is about destructuring after the instanceof/nullcheck has been done, so i find that argument counter productive because it does not help to understand the semantics. > > Also, we have talked several times to introduce the record pattern when doing an assignment > Point point = ... > Point(int x, int y) = point; > // can use x and y here ! > > This will throw a NPE if point is null, similarly to an unboxing operation. > > The null-safety is not attached to the record pattern per se but by the container that use it (instanceof, case of a switch, enclosing pattern). > > regards, > R?mi > > ----- Original Message ----- >> From: "Gavin Bierman" >> To: "amber-spec-experts" >> Sent: Wednesday, February 9, 2022 12:59:02 PM >> Subject: JEP 405 update > >> Dear experts, >> >> Just to let you know that I have updated JEP 405: >> >> https://openjdk.java.net/jeps/405 >> >> You will see that we have removed the array patterns from this JEP (and it has >> been retitled accordingly). We're still committed to supporting a direct pattern >> form for arrays, but given our tight schedule and a number of queries about the >> exact syntactic form for array patterns, we think we'll be better off decoupling >> them from JEP 405, and releasing them in a future patterns JEP. >> >> Comments welcomed! >> Gavin From forax at univ-mlv.fr Thu Feb 10 16:21:11 2022 From: forax at univ-mlv.fr (forax at univ-mlv.fr) Date: Thu, 10 Feb 2022 17:21:11 +0100 (CET) Subject: [External] : Re: JEP 405 update In-Reply-To: <4B9F216A-CF2A-467F-A4ED-596EC39220F3@oracle.com> References: <68568565.1120088.1644424808438.JavaMail.zimbra@u-pem.fr> <4B9F216A-CF2A-467F-A4ED-596EC39220F3@oracle.com> Message-ID: <1456145295.1783332.1644510071066.JavaMail.zimbra@u-pem.fr> ----- Original Message ----- > From: "Gavin Bierman" > To: "Remi Forax" > Cc: "amber-spec-experts" > Sent: Wednesday, February 9, 2022 11:11:56 PM > Subject: Re: [External] : Re: JEP 405 update > Thanks Remi. Yes, this text could be better (interestingly, it is unchanged from > nearly a year ago!). I believe it's a direct consequence of this JEP being more readable now that it was before. It was covering too mush ground at too high level. > I think you?d agree that *pattern matching* with nested > patterns provides a null-safe approach to data access, right? Not more than the type pattern Object o = ... switch(o) { case Bar bar -> // bar can not be null here default -> ... } or Object o = ... if o instanceof Bar bar) { // bar can not be null here } In a sense, the record pattern is an "extension" of the type pattern so like the type pattern it does not allow null (if not total). BTW, the record pattern is not an extension of the type pattern if the type is total, It's another clue that the type pattern and totality are two different concepts. We would not have such issue if the type pattern was separated into two kind of patterns, the type pattern (case Foo foo) that requires an explicit type and the var pattern (case var foo) that is total (again the C# solution). > That?s really what I am getting at. In that sense, record patterns are a key, because they > support nesting of patterns. You are correct that the construct within which we > surface pattern matching can have a say in the top-level treatment of null. It's not what i've said, the treatment of null is due to the fact that a type pattern (at least the non total one) is semantically an instanceof thus reject null. > Indeed, one of the things we are discussing at the moment is exactly whether > switch should have a stronger opinion about top-level null! yes i know, and i find that idea weird, we should try to make the patterns to behave the same way whatever the container, to not bother users with to many details. > > Let me see if I can work on the text some more. > > Thanks for the feedback, > Gavin R?mi > >> On 9 Feb 2022, at 16:40, Remi Forax wrote: >> >> Hi Gavin, >> I don't buy the argument that record patterns promote a null-safe style of >> programming as this is stated several times in the JEP. >> >> The ""null-safety"" (notice the air quotes) mostly comes from the instanceof or >> the switch (which semantics is equivalent of a cascade of if instanceof), not >> from the record pattern by itself. >> >> You can argue that when a record pattern is nested a nullcheck appears, but it's >> more than the underlying semantics is a degenerated instanceof when the >> declared type and the instanceof type are the same. >> >> The record pattern is about destructuring after the instanceof/nullcheck has >> been done, so i find that argument counter productive because it does not help >> to understand the semantics. >> >> Also, we have talked several times to introduce the record pattern when doing an >> assignment >> Point point = ... >> Point(int x, int y) = point; >> // can use x and y here ! >> >> This will throw a NPE if point is null, similarly to an unboxing operation. >> >> The null-safety is not attached to the record pattern per se but by the >> container that use it (instanceof, case of a switch, enclosing pattern). >> >> regards, >> R?mi >> >> ----- Original Message ----- >>> From: "Gavin Bierman" >>> To: "amber-spec-experts" >>> Sent: Wednesday, February 9, 2022 12:59:02 PM >>> Subject: JEP 405 update >> >>> Dear experts, >>> >>> Just to let you know that I have updated JEP 405: >>> >>> https://openjdk.java.net/jeps/405 >>> >>> You will see that we have removed the array patterns from this JEP (and it has >>> been retitled accordingly). We're still committed to supporting a direct pattern >>> form for arrays, but given our tight schedule and a number of queries about the >>> exact syntactic form for array patterns, we think we'll be better off decoupling >>> them from JEP 405, and releasing them in a future patterns JEP. >>> >>> Comments welcomed! > >> Gavin From brian.goetz at oracle.com Thu Feb 10 17:12:51 2022 From: brian.goetz at oracle.com (Brian Goetz) Date: Thu, 10 Feb 2022 12:12:51 -0500 Subject: [External] : Re: JEP 405 update In-Reply-To: <1456145295.1783332.1644510071066.JavaMail.zimbra@u-pem.fr> References: <68568565.1120088.1644424808438.JavaMail.zimbra@u-pem.fr> <4B9F216A-CF2A-467F-A4ED-596EC39220F3@oracle.com> <1456145295.1783332.1644510071066.JavaMail.zimbra@u-pem.fr> Message-ID: <9f929a42-6061-9d02-5178-a5bdf7c7381c@oracle.com> >> That?s really what I am getting at. In that sense, record patterns are a key, because they >> support nesting of patterns. You are correct that the construct within which we >> surface pattern matching can have a say in the top-level treatment of null. > It's not what i've said, the treatment of null is due to the fact that a type pattern (at least the non total one) is semantically an instanceof thus reject null. We have been over this over and over again.? This is not right, and you are confusing people. There is a predicate for pattern matching, `matches(P, T, e)`.? It is defined only in terms of the pattern, the target type, and an expression to match against it.? If P="Object o", and T="String", then matches(P, T, null) is true.? This is the semantics of _patterns_.? Total patterns match null. Patterns can appear in multiple _contexts_, such as instanceof and switch today, and soon, nested pattern contexts, and maybe in the future, catch clauses or pattern assignment.? The context can appeal to the semantics of pattern matching.? So for ??? x instanceof P the instanceof context first tests x for null, evaluating to false if so, and otherwise appeals to matches(P, typeof(x), x).? For switch, the switch context first tests the target for null, and, if the switch has no null cases, throws NPE, otherwise it starts appealling to pattern matching.? In a nested context, there is no such extra context-specific logic.? In other contexts, such as let-bind, there will be similar remainder-rejection.? But switch throwing NPE on null has nothing to do with whether the patterns match null. The two are separate; you are conflating them.? I realize you would like it to work differently, but please stop incorrectly characterizing how it does work. > yes i know, and i find that idea weird, we should try to make the > patterns to behave the same way whatever the container, to not bother > users with to many details. They do work the same regardless of container.? It is the *containers* that have their own additional logic, that is executed first, before we attempt to match any patterns. From forax at univ-mlv.fr Thu Feb 10 17:30:21 2022 From: forax at univ-mlv.fr (forax at univ-mlv.fr) Date: Thu, 10 Feb 2022 18:30:21 +0100 (CET) Subject: [External] : Re: JEP 405 update In-Reply-To: <9f929a42-6061-9d02-5178-a5bdf7c7381c@oracle.com> References: <68568565.1120088.1644424808438.JavaMail.zimbra@u-pem.fr> <4B9F216A-CF2A-467F-A4ED-596EC39220F3@oracle.com> <1456145295.1783332.1644510071066.JavaMail.zimbra@u-pem.fr> <9f929a42-6061-9d02-5178-a5bdf7c7381c@oracle.com> Message-ID: <794186875.1838691.1644514221637.JavaMail.zimbra@u-pem.fr> ----- Original Message ----- > From: "Brian Goetz" > To: "Remi Forax" , "Gavin Bierman" > Cc: "amber-spec-experts" > Sent: Thursday, February 10, 2022 6:12:51 PM > Subject: Re: [External] : Re: JEP 405 update >>> That?s really what I am getting at. In that sense, record patterns are a key, >>> because they >>> support nesting of patterns. You are correct that the construct within which we >>> surface pattern matching can have a say in the top-level treatment of null. >> It's not what i've said, the treatment of null is due to the fact that a type >> pattern (at least the non total one) is semantically an instanceof thus reject >> null. > > We have been over this over and over again.? This is not right, and you > are confusing people. I think you misunderstood me, it's my fault i've used the term "reject" instead of "not match", i was not talking about NPE here but the fact that both non total type pattern and record pattern does not match null. The fact that the switch can throw a NPE or not is a property of the record pattern, that was my point. R?mi From brian.goetz at oracle.com Tue Feb 15 18:50:06 2022 From: brian.goetz at oracle.com (Brian Goetz) Date: Tue, 15 Feb 2022 13:50:06 -0500 Subject: Reviewing feedback on patterns in switch In-Reply-To: <06062579-A7DD-499C-839D-B77A319E38ED@oracle.com> References: <06062579-A7DD-499C-839D-B77A319E38ED@oracle.com> Message-ID: 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. -------------- next part -------------- An HTML attachment was scrubbed... URL: From forax at univ-mlv.fr Tue Feb 15 22:58:41 2022 From: forax at univ-mlv.fr (Remi Forax) Date: Tue, 15 Feb 2022 23:58:41 +0100 (CET) Subject: Reviewing feedback on patterns in switch In-Reply-To: References: <06062579-A7DD-499C-839D-B77A319E38ED@oracle.com> Message-ID: <441758451.4057310.1644965921030.JavaMail.zimbra@u-pem.fr> > 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 > than with &&. 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. > tl;dr: inventing a `when` modifier on switch now will save us from having to > invent something else in the future; choosing && will not. I want to be convinced, but i'm not, the argument about the precedence is exactly why we have chosen && in the first place. R?mi -------------- next part -------------- An HTML attachment was scrubbed... URL: From guy.steele at oracle.com Wed Feb 16 02:55:37 2022 From: guy.steele at oracle.com (Guy Steele) Date: Wed, 16 Feb 2022 02:55:37 +0000 Subject: Reviewing feedback on patterns in switch In-Reply-To: <441758451.4057310.1644965921030.JavaMail.zimbra@u-pem.fr> References: <06062579-A7DD-499C-839D-B77A319E38ED@oracle.com> <441758451.4057310.1644965921030.JavaMail.zimbra@u-pem.fr> Message-ID: <4B9EA50B-38FB-45F7-97AB-75B3A30E43E7@oracle.com> On Feb 15, 2022, at 5:58 PM, Remi Forax > wrote: ________________________________ 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. 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 { 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 than with &&. 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. tl;dr: inventing a `when` modifier on switch now will save us from having to invent something else in the future; choosing && will not. I want to be convinced, but i'm not, the argument about the precedence is exactly why we have chosen && in the first place. R?mi -------------- next part -------------- An HTML attachment was scrubbed... URL: From brian.goetz at oracle.com Wed Feb 16 14:48:14 2022 From: brian.goetz at oracle.com (Brian Goetz) Date: Wed, 16 Feb 2022 09:48:14 -0500 Subject: [External] : Re: Reviewing feedback on patterns in switch In-Reply-To: <441758451.4057310.1644965921030.JavaMail.zimbra@u-pem.fr> References: <06062579-A7DD-499C-839D-B77A319E38ED@oracle.com> <441758451.4057310.1644965921030.JavaMail.zimbra@u-pem.fr> Message-ID: > 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. -------------- next part -------------- An HTML attachment was scrubbed... URL: From brian.goetz at oracle.com Wed Feb 16 14:57:39 2022 From: brian.goetz at oracle.com (Brian Goetz) Date: Wed, 16 Feb 2022 09:57:39 -0500 Subject: [External] : Re: Reviewing feedback on patterns in switch In-Reply-To: <441758451.4057310.1644965921030.JavaMail.zimbra@u-pem.fr> References: <06062579-A7DD-499C-839D-B77A319E38ED@oracle.com> <441758451.4057310.1644965921030.JavaMail.zimbra@u-pem.fr> Message-ID: 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. -------------- next part -------------- An HTML attachment was scrubbed... URL: From brian.goetz at oracle.com Wed Feb 16 15:05:35 2022 From: brian.goetz at oracle.com (Brian Goetz) Date: Wed, 16 Feb 2022 10:05:35 -0500 Subject: Reviewing feedback on patterns in switch In-Reply-To: <4B9EA50B-38FB-45F7-97AB-75B3A30E43E7@oracle.com> References: <06062579-A7DD-499C-839D-B77A319E38ED@oracle.com> <441758451.4057310.1644965921030.JavaMail.zimbra@u-pem.fr> <4B9EA50B-38FB-45F7-97AB-75B3A30E43E7@oracle.com> Message-ID: <43689608-e6f7-76f0-3b8b-df9ed4756d24@oracle.com> >> >> 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: -------------- next part -------------- An HTML attachment was scrubbed... URL: From brian.goetz at oracle.com Wed Feb 16 15:49:19 2022 From: brian.goetz at oracle.com (Brian Goetz) Date: Wed, 16 Feb 2022 10:49:19 -0500 Subject: Reviewing feedback on patterns in switch In-Reply-To: <06062579-A7DD-499C-839D-B77A319E38ED@oracle.com> References: <06062579-A7DD-499C-839D-B77A319E38ED@oracle.com> Message-ID: 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) > > -------------- next part -------------- An HTML attachment was scrubbed... URL: From forax at univ-mlv.fr Wed Feb 16 16:00:15 2022 From: forax at univ-mlv.fr (Remi Forax) Date: Wed, 16 Feb 2022 17:00:15 +0100 (CET) Subject: Reviewing feedback on patterns in switch In-Reply-To: References: <06062579-A7DD-499C-839D-B77A319E38ED@oracle.com> Message-ID: <940617922.4409788.1645027215798.JavaMail.zimbra@u-pem.fr> > 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) -------------- next part -------------- An HTML attachment was scrubbed... URL: From brian.goetz at oracle.com Wed Feb 16 16:03:19 2022 From: brian.goetz at oracle.com (Brian Goetz) Date: Wed, 16 Feb 2022 11:03:19 -0500 Subject: [External] : Re: Reviewing feedback on patterns in switch In-Reply-To: <940617922.4409788.1645027215798.JavaMail.zimbra@u-pem.fr> References: <06062579-A7DD-499C-839D-B77A319E38ED@oracle.com> <940617922.4409788.1645027215798.JavaMail.zimbra@u-pem.fr> Message-ID: <97d12d2c-32d9-d679-b585-a39ffd7782a2@oracle.com> 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) > > > > -------------- next part -------------- An HTML attachment was scrubbed... URL: From forax at univ-mlv.fr Wed Feb 16 16:03:14 2022 From: forax at univ-mlv.fr (forax at univ-mlv.fr) Date: Wed, 16 Feb 2022 17:03:14 +0100 (CET) Subject: Reviewing feedback on patterns in switch In-Reply-To: <43689608-e6f7-76f0-3b8b-df9ed4756d24@oracle.com> References: <06062579-A7DD-499C-839D-B77A319E38ED@oracle.com> <441758451.4057310.1644965921030.JavaMail.zimbra@u-pem.fr> <4B9EA50B-38FB-45F7-97AB-75B3A30E43E7@oracle.com> <43689608-e6f7-76f0-3b8b-df9ed4756d24@oracle.com> Message-ID: <444735198.4410666.1645027394425.JavaMail.zimbra@u-pem.fr> > 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 -------------- next part -------------- An HTML attachment was scrubbed... URL: From mark at io7m.com Wed Feb 16 16:05:20 2022 From: mark at io7m.com (Mark Raynsford) Date: Wed, 16 Feb 2022 16:05:20 +0000 Subject: Reviewing feedback on patterns in switch In-Reply-To: References: <06062579-A7DD-499C-839D-B77A319E38ED@oracle.com> Message-ID: <20220216160520.223bc50a@sunflower.int.arc7.info> 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 From forax at univ-mlv.fr Wed Feb 16 16:18:06 2022 From: forax at univ-mlv.fr (forax at univ-mlv.fr) Date: Wed, 16 Feb 2022 17:18:06 +0100 (CET) Subject: [External] : Re: Reviewing feedback on patterns in switch In-Reply-To: References: <06062579-A7DD-499C-839D-B77A319E38ED@oracle.com> <441758451.4057310.1644965921030.JavaMail.zimbra@u-pem.fr> Message-ID: <414269084.4415579.1645028286910.JavaMail.zimbra@u-pem.fr> > 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 -------------- next part -------------- An HTML attachment was scrubbed... URL: From forax at univ-mlv.fr Wed Feb 16 16:19:02 2022 From: forax at univ-mlv.fr (forax at univ-mlv.fr) Date: Wed, 16 Feb 2022 17:19:02 +0100 (CET) Subject: [External] : Re: Reviewing feedback on patterns in switch In-Reply-To: References: <06062579-A7DD-499C-839D-B77A319E38ED@oracle.com> <441758451.4057310.1644965921030.JavaMail.zimbra@u-pem.fr> Message-ID: <859878927.4415708.1645028342827.JavaMail.zimbra@u-pem.fr> > 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. -------------- next part -------------- An HTML attachment was scrubbed... URL: From forax at univ-mlv.fr Wed Feb 16 16:23:46 2022 From: forax at univ-mlv.fr (forax at univ-mlv.fr) Date: Wed, 16 Feb 2022 17:23:46 +0100 (CET) Subject: [External] : Re: Reviewing feedback on patterns in switch In-Reply-To: <97d12d2c-32d9-d679-b585-a39ffd7782a2@oracle.com> References: <06062579-A7DD-499C-839D-B77A319E38ED@oracle.com> <940617922.4409788.1645027215798.JavaMail.zimbra@u-pem.fr> <97d12d2c-32d9-d679-b585-a39ffd7782a2@oracle.com> Message-ID: <896754333.4416939.1645028626686.JavaMail.zimbra@u-pem.fr> > 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.goetz at oracle.com | ] >>> To: "amber-spec-experts" [ mailto:amber-spec-experts at 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) -------------- next part -------------- An HTML attachment was scrubbed... URL: From guy.steele at oracle.com Wed Feb 16 16:42:55 2022 From: guy.steele at oracle.com (Guy Steele) Date: Wed, 16 Feb 2022 16:42:55 +0000 Subject: [External] : Re: Reviewing feedback on patterns in switch In-Reply-To: References: <06062579-A7DD-499C-839D-B77A319E38ED@oracle.com> <441758451.4057310.1644965921030.JavaMail.zimbra@u-pem.fr> Message-ID: <99F72A4E-8D9E-475C-B1B6-9EE4DE2FB273@oracle.com> > 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! :-) From brian.goetz at oracle.com Wed Feb 16 18:34:17 2022 From: brian.goetz at oracle.com (Brian Goetz) Date: Wed, 16 Feb 2022 13:34:17 -0500 Subject: Record patterns (and beyond): exceptions Message-ID: <84f64bf3-7803-48d3-b94b-8b0640a20864@oracle.com> As we move towards the next deliverable -- record patterns -- we have two new questions regarding exceptions to answer. #### Questions 1.? When a dtor throws an exception.? (You might think we can kick this down the road, since records automatically acquire a synthetic dtor, and users can't write dtors yet, but since that synthetic dtor will invoke record component accessors, and users can override record component accessors and therefore they can throw, we actually need to deal with this now.) This has two sub-questions: 1a.? Do we want to do any extra type checking on the bodies of dtors / record accessors to discourage explicitly throwing exceptions?? Obviously we cannot prevent exceptions like NPEs arising out of dereference, but we could warn on an explicit throw statement in a record accessor / dtor declaration, to remind users that throwing from dtors is not the droid they are looking for. 1b.? When the dtor for Foo in the switch statement below throws E: ??? switch (x) { ??????? case Box(Foo(var a)): ... ??????? case Box(Bar(var b)): ... ??? } what should happen?? Candidates include: ?- allow the switch to complete abruptly by throwing E? ?- same, but wrap E in some sort of ExceptionInMatcherException? ?- ignore the exception and treat the match as having failed, and move on to the next case? 2.? Exceptions for remainder.? We've established that there is a difference between an _exhaustive_ set of patterns (one good enough to satisfy the compiler that the switch is complete enough) and a _total_ set of patterns (one that actually covers all input values.)? The difference is called the _remainder_.? For constructs that require totality, such as pattern switches and let/bind, we have invariants about what will have happened if the construct completes normally; for switches, this means exactly one of the cases was selected.? The compiler must make up the difference by inserting a throwing catch-all, as we already do with expression switches over enums, and all switches over sealed types, that lack total/default clauses. So far, remainder is restricted to two kinds of values: null (about which switch already has a strong opinion) and "novel" enum constants / novel subtypes of sealed types. For the former, we throw NPE; for the latter, we throw ICCE. As we look ahead to record patterns, there is a new kind of remainder: the "spine" of nested record patterns.? This includes things like Box(null), Box(novel), Box(Bag(null)), Box(Mapping(null, novel)), etc.? It should be clear that there is no clean extrapolation from what we currently do, to what we should do in these cases.? But that's OK; both of the existing remainder-rejection cases derive from "what does the context think" -- switch hates null (so, NPE), and enum switches are a thing (which makes ICCE on an enum switch reasonable.)? But in the general case, we'll want some sort of MatchRemainderException. Note that throwing an exception from remainder is delayed until the last possible moment.? We could have: ??? case Box(Bag(var x)): ... ??? case Box(var x) when x == null: ... and the reasonable treatment is to treat Box(Bag(var x)) as not matching Box(null), even though it is exhuastive on Box>), and therefore fall into the second case on Box(null).? Only when we reach the end of the switch, and we haven't matched any cases, do we throw MatchRemainderException. #### Discussion For (1a), my inclination is to do nothing for record accessors, but when we get to explicit dtors, warning on explicit throw is not a bad idea.? Unlike ctors, where exceptions are part of the standard toolbox, deconstructors are handed an already-constructed object, and are supposed to be total.? If you're inclined to write a partial dtor, you're probably doing it wrong.? As it is a new construct, the additional error checking to guide people to its proper use is probably reasonable (and cheap.) For (1b), since an exception in a dtor is suppose to indicate an exceptional failure, I don't think swallowing it and trying to go on with the show is a good move.? My preference would be to wrap the exception, as we do with ExceptionInInitializerError, to make it clear that an exception from a dtor is a truly unexpected thing, and clearly name-and-shame the offending dtor.? So there's a bikeshed to paint for what we call this exception. For (2), trying to repurpose either NPE or ICCE here is a losing move.? Better to invent an exception type that means "uncovered remainder" (which is more akin to an IAE than anything else; someone passed a bad value to an exhaustive-enough switch.)? We would use the same exception in let/bind, so this shouldn't have "switch" in its name, but probably something more like MatchRemainderException. -------------- next part -------------- An HTML attachment was scrubbed... URL: From manoj.palat at in.ibm.com Wed Feb 16 23:08:35 2022 From: manoj.palat at in.ibm.com (Manoj Palat) Date: Wed, 16 Feb 2022 23:08:35 +0000 Subject: [JEP 420] Spec Change for switch exhaustive with sealed Message-ID: Hi Gavin, all, For the issue ?Switch coverage with multiple branches? raised in https://mail.openjdk.java.net/pipermail/amber-spec-experts/2021-July/003049.html by Dan [yeah ? I know it?s a little late to ask this, but still :( ], I couldn?t figure out a spec change in JEP 420 - from the relevant part, it says the following: ?T supports a sealed class or interface named C, and the switch block is exhaustive for all the permitted direct subclasses and subinterfaces of C that are applicable at type T.? And it still says ?exhaustive for all the permitted?? However, I see that JDK18 accepts the code mentioned in the mail [via JDK-8274363 /b18]. Is there a spec change expected in the next version where this would be made explicit or is this ?exhaustiveness wrt permitted classes? assumed to be an implementation detail? Regards, Manoj -------------- next part -------------- An HTML attachment was scrubbed... URL: From amaembo at gmail.com Thu Feb 17 08:46:18 2022 From: amaembo at gmail.com (Tagir Valeev) Date: Thu, 17 Feb 2022 15:46:18 +0700 Subject: Record patterns (and beyond): exceptions In-Reply-To: <84f64bf3-7803-48d3-b94b-8b0640a20864@oracle.com> References: <84f64bf3-7803-48d3-b94b-8b0640a20864@oracle.com> Message-ID: Hello! I like the comparison with ExceptionInInitializerError and want to draw more parallels to static initializers. People rarely expect an exception popping when you do something harmless like reading a static field (likely it's much less expected than exception from pattern), so it's usually a bad idea to throw from static initializer. Yet, we allow `throw` statements there. What we don't allow is unconditional throwing (static initializer must be able to complete normally, not abruptly). And yes, we do wrap the exception. It's especially important, as a foreign class could throw a checked exception (if compiled not from Java or using weird Java extensions like Lombok), and seeing the checked exception popping out of nowhere is especially bad (in particular, because you cannot catch it specifically in Java, it will be a compilation error). I think it's reasonable to implement a similar approach for deconstructors: disallow always-abrupt deconstructor, and wrap the exceptions. With best regards, Tagir Valeev On Thu, Feb 17, 2022 at 1:35 AM Brian Goetz wrote: > > As we move towards the next deliverable -- record patterns -- we have two new questions regarding exceptions to answer. > > #### Questions > > 1. When a dtor throws an exception. (You might think we can kick this down the road, since records automatically acquire a synthetic dtor, and users can't write dtors yet, but since that synthetic dtor will invoke record component accessors, and users can override record component accessors and therefore they can throw, we actually need to deal with this now.) > > This has two sub-questions: > > 1a. Do we want to do any extra type checking on the bodies of dtors / record accessors to discourage explicitly throwing exceptions? Obviously we cannot prevent exceptions like NPEs arising out of dereference, but we could warn on an explicit throw statement in a record accessor / dtor declaration, to remind users that throwing from dtors is not the droid they are looking for. > > 1b. When the dtor for Foo in the switch statement below throws E: > > switch (x) { > case Box(Foo(var a)): ... > case Box(Bar(var b)): ... > } > > what should happen? Candidates include: > > - allow the switch to complete abruptly by throwing E? > - same, but wrap E in some sort of ExceptionInMatcherException? > - ignore the exception and treat the match as having failed, and move on to the next case? > > 2. Exceptions for remainder. We've established that there is a difference between an _exhaustive_ set of patterns (one good enough to satisfy the compiler that the switch is complete enough) and a _total_ set of patterns (one that actually covers all input values.) The difference is called the _remainder_. For constructs that require totality, such as pattern switches and let/bind, we have invariants about what will have happened if the construct completes normally; for switches, this means exactly one of the cases was selected. The compiler must make up the difference by inserting a throwing catch-all, as we already do with expression switches over enums, and all switches over sealed types, that lack total/default clauses. > > So far, remainder is restricted to two kinds of values: null (about which switch already has a strong opinion) and "novel" enum constants / novel subtypes of sealed types. For the former, we throw NPE; for the latter, we throw ICCE. > > As we look ahead to record patterns, there is a new kind of remainder: the "spine" of nested record patterns. This includes things like Box(null), Box(novel), Box(Bag(null)), Box(Mapping(null, novel)), etc. It should be clear that there is no clean extrapolation from what we currently do, to what we should do in these cases. But that's OK; both of the existing remainder-rejection cases derive from "what does the context think" -- switch hates null (so, NPE), and enum switches are a thing (which makes ICCE on an enum switch reasonable.) But in the general case, we'll want some sort of MatchRemainderException. > > Note that throwing an exception from remainder is delayed until the last possible moment. We could have: > > case Box(Bag(var x)): ... > case Box(var x) when x == null: ... > > and the reasonable treatment is to treat Box(Bag(var x)) as not matching Box(null), even though it is exhuastive on Box>), and therefore fall into the second case on Box(null). Only when we reach the end of the switch, and we haven't matched any cases, do we throw MatchRemainderException. > > #### Discussion > > For (1a), my inclination is to do nothing for record accessors, but when we get to explicit dtors, warning on explicit throw is not a bad idea. Unlike ctors, where exceptions are part of the standard toolbox, deconstructors are handed an already-constructed object, and are supposed to be total. If you're inclined to write a partial dtor, you're probably doing it wrong. As it is a new construct, the additional error checking to guide people to its proper use is probably reasonable (and cheap.) > > For (1b), since an exception in a dtor is suppose to indicate an exceptional failure, I don't think swallowing it and trying to go on with the show is a good move. My preference would be to wrap the exception, as we do with ExceptionInInitializerError, to make it clear that an exception from a dtor is a truly unexpected thing, and clearly name-and-shame the offending dtor. So there's a bikeshed to paint for what we call this exception. > > For (2), trying to repurpose either NPE or ICCE here is a losing move. Better to invent an exception type that means "uncovered remainder" (which is more akin to an IAE than anything else; someone passed a bad value to an exhaustive-enough switch.) We would use the same exception in let/bind, so this shouldn't have "switch" in its name, but probably something more like MatchRemainderException. > > From forax at univ-mlv.fr Thu Feb 17 10:45:18 2022 From: forax at univ-mlv.fr (Remi Forax) Date: Thu, 17 Feb 2022 11:45:18 +0100 (CET) Subject: Record patterns (and beyond): exceptions In-Reply-To: <84f64bf3-7803-48d3-b94b-8b0640a20864@oracle.com> References: <84f64bf3-7803-48d3-b94b-8b0640a20864@oracle.com> Message-ID: <1564273027.4805699.1645094718556.JavaMail.zimbra@u-pem.fr> > From: "Brian Goetz" > To: "amber-spec-experts" > Sent: Wednesday, February 16, 2022 7:34:17 PM > Subject: Record patterns (and beyond): exceptions > As we move towards the next deliverable -- record patterns -- we have two new > questions regarding exceptions to answer. > #### Questions > 1. When a dtor throws an exception. ( You might think we can kick this down the > road, since records automatically acquire a synthetic dtor, and users can't > write dtors yet, but since that synthetic dtor will invoke record component > accessors, and users can override record component accessors and therefore they > can throw, we actually need to deal with this now.) > This has two sub-questions: > 1a. Do we want to do any extra type checking on the bodies of dtors / record > accessors to discourage explicitly throwing exceptions? Obviously we cannot > prevent exceptions like NPEs arising out of dereference, but we could warn on > an explicit throw statement in a record accessor / dtor declaration, to remind > users that throwing from dtors is not the droid they are looking for. For de-constructor, given that they does not exist yet, we can do like record constructor, banned checked exceptions and for accessors, we can emit a warning as you suggest and do not allow record pattern if one of the getters throws a checked exception. > 1b. When the dtor for Foo in the switch statement below throws E: > switch (x) { > case Box(Foo(var a)): ... > case Box(Bar(var b)): ... > } > what should happen? Candidates include: > - allow the switch to complete abruptly by throwing E? > - same, but wrap E in some sort of ExceptionInMatcherException? > - ignore the exception and treat the match as having failed, and move on to the > next case? The nice thing about the rules above is that a record pattern can never throw a checked exception. So there is nothing to do here. > 2. Exceptions for remainder. We've established that there is a difference > between an _exhaustive_ set of patterns (one good enough to satisfy the > compiler that the switch is complete enough) and a _total_ set of patterns (one > that actually covers all input values.) The difference is called the > _remainder_. For constructs that require totality, such as pattern switches and > let/bind, we have invariants about what will have happened if the construct > completes normally; for switches, this means exactly one of the cases was > selected. The compiler must make up the difference by inserting a throwing > catch-all, as we already do with expression switches over enums, and all > switches over sealed types, that lack total/default clauses. > So far, remainder is restricted to two kinds of values: null (about which switch > already has a strong opinion) and "novel" enum constants / novel subtypes of > sealed types. For the former, we throw NPE; for the latter, we throw ICCE. > As we look ahead to record patterns, there is a new kind of remainder: the > "spine" of nested record patterns. This includes things like Box(null), > Box(novel), Box(Bag(null)), Box(Mapping(null, novel)), etc. It should be clear > that there is no clean extrapolation from what we currently do, to what we > should do in these cases. But that's OK; both of the existing > remainder-rejection cases derive from "what does the context think" -- switch > hates null (so, NPE), and enum switches are a thing (which makes ICCE on an > enum switch reasonable.) But in the general case, we'll want some sort of > MatchRemainderException. Nope, it can not be a runtime exception because people will write code to catch it and we will have a boat load of subtle bugs because exception are side effect so you can see in which order the de-constructors or the pattern methods are called. ICCE is fine. > Note that throwing an exception from remainder is delayed until the last > possible moment. We could have: > case Box(Bag(var x)): ... > case Box(var x) when x == null: ... > and the reasonable treatment is to treat Box(Bag(var x)) as not matching > Box(null), even though it is exhuastive on Box>), and therefore fall > into the second case on Box(null). Only when we reach the end of the switch, > and we haven't matched any cases, do we throw MatchRemainderException. I really dislike that idea, it will be a burden in the future each time we want to change the implementation. I would like the semantics to make no promise about when the error will be thrown, the semantics should not be defined if a deconstructors/pattern method does a side effect, the same way the stream semantics is not defined if the lambda taken as parameter of Stream.map() does a side effect. I think the parallel between the pattern matching and a stream in term of execution semantics is important here. From the outside, those things are monads, they should work the same way. R?mi -------------- next part -------------- An HTML attachment was scrubbed... URL: From brian.goetz at oracle.com Thu Feb 17 14:43:08 2022 From: brian.goetz at oracle.com (Brian Goetz) Date: Thu, 17 Feb 2022 09:43:08 -0500 Subject: [External] : Re: Record patterns (and beyond): exceptions In-Reply-To: <1564273027.4805699.1645094718556.JavaMail.zimbra@u-pem.fr> References: <84f64bf3-7803-48d3-b94b-8b0640a20864@oracle.com> <1564273027.4805699.1645094718556.JavaMail.zimbra@u-pem.fr> Message-ID: > > As we look ahead to record patterns, there is a new kind of > remainder: the "spine" of nested record patterns.? This includes > things like Box(null), Box(novel), Box(Bag(null)), > Box(Mapping(null, novel)), etc.? It should be clear that there is > no clean extrapolation from what we currently do, to what we > should do in these cases.? But that's OK; both of the existing > remainder-rejection cases derive from "what does the context > think" -- switch hates null (so, NPE), and enum switches are a > thing (which makes ICCE on an enum switch reasonable.) But in the > general case, we'll want some sort of MatchRemainderException. > > > Nope, it can not be a runtime exception because people will write code > to catch it and we will have a boat load of subtle bugs because > exception are side effect so you can see in which order the > de-constructors or the pattern methods are called. ICCE is fine. But this clearly does not fall into ICCE.? ICCE means, basically, "your classpath is borked"; that things that were known to be true at compile time are not true at runtime.? (Inconsistent separate compilation is the most common cause.)? But Box(Bag(null)) is not an artifact of inconsistent separate compilation. In any case, I am not getting your point about "but people can catch it."? So what?? People can catch OOME too, and try to parse the output of toString() when we tell them not to.? But that's no reason to make all exceptions "OpaqueError".? So what is your point here? > > > Note that throwing an exception from remainder is delayed until > the last possible moment.? We could have: > > ??? case Box(Bag(var x)): ... > ??? case Box(var x) when x == null: ... > > and the reasonable treatment is to treat Box(Bag(var x)) as not > matching Box(null), even though it is exhuastive on Box>), > and therefore fall into the second case on Box(null). Only when we > reach the end of the switch, and we haven't matched any cases, do > we throw MatchRemainderException. > > > I really dislike that idea, it will be a burden in the future each > time we want to change the implementation. > I would like the semantics to make no promise about when the error > will be thrown, the semantics should not be defined if a > deconstructors/pattern method does a side effect, the same way the > stream semantics is not defined if the lambda taken as parameter of > Stream.map() does a side effect. > I think the parallel between the pattern matching and a stream in term > of execution semantics is important here. From the outside, those > things are monads, they should work the same way. > > I think this stems from the same misunderstanding you have about the boundary between the pattern semantics and the construct semantics. I'm going to test-drive some adjusted language here. A total pattern is just that -- it matches everything. Some patterns are considered exhaustive, but not total.? A deconstruction pattern D(E(total)) is one such example; it is exhaustive on D, but does not match D(null), because matching the nested E(total) requires invoking a deconstructor in E, and you can't invoke an instance member on a null receiver.? Still, we consider D(E(total)) exhaustive on D, which means it is enough to satisfy the type checker that you've covered everything. Remainder is just the gap between exhaustiveness and totality. If we have the following switch: ??? case D(E(Object o)): ??? caes D(var x) when x == null: the semantics of D(E(Object)) are that *it matches all non-null D, except D(null)*.? So throwing when we evaluate the case would be incorrect; switch asks the pattern "do you match", and the pattern says "no, I do not."? And the semantics of switch, then, say "then I will keep trying the rest of the cases." So *when* the error is thrown derives from the semantics of the construct; switch tries matching with each pattern, until it finds a match or runs out of patterns.? When it runs out of patterns is when it needs to insert a catch-all to deal with remainder (as we do with enum switch expressions.) -------------- next part -------------- An HTML attachment was scrubbed... URL: From forax at univ-mlv.fr Fri Feb 18 07:45:57 2022 From: forax at univ-mlv.fr (forax at univ-mlv.fr) Date: Fri, 18 Feb 2022 08:45:57 +0100 (CET) Subject: [External] : Re: Record patterns (and beyond): exceptions In-Reply-To: References: <84f64bf3-7803-48d3-b94b-8b0640a20864@oracle.com> <1564273027.4805699.1645094718556.JavaMail.zimbra@u-pem.fr> Message-ID: <756246617.5170118.1645170357069.JavaMail.zimbra@u-pem.fr> > From: "Brian Goetz" > To: "Remi Forax" > Cc: "amber-spec-experts" > Sent: Thursday, February 17, 2022 3:43:08 PM > Subject: Re: [External] : Re: Record patterns (and beyond): exceptions >>> As we look ahead to record patterns, there is a new kind of remainder: the >>> "spine" of nested record patterns. This includes things like Box(null), >>> Box(novel), Box(Bag(null)), Box(Mapping(null, novel)), etc. It should be clear >>> that there is no clean extrapolation from what we currently do, to what we >>> should do in these cases. But that's OK; both of the existing >>> remainder-rejection cases derive from "what does the context think" -- switch >>> hates null (so, NPE), and enum switches are a thing (which makes ICCE on an >>> enum switch reasonable.) But in the general case, we'll want some sort of >>> MatchRemainderException. >> Nope, it can not be a runtime exception because people will write code to catch >> it and we will have a boat load of subtle bugs because exception are side >> effect so you can see in which order the de-constructors or the pattern methods >> are called. ICCE is fine. > But this clearly does not fall into ICCE. ICCE means, basically, "your classpath > is borked"; that things that were known to be true at compile time are not true > at runtime. (Inconsistent separate compilation is the most common cause.) But > Box(Bag(null)) is not an artifact of inconsistent separate compilation. I think i've not understood the problem correctly, i was thinking the error was due to the erasure, Box being erased to Box, the problem with erasure is that you see the problem late, in case of the switch after the phase that does instanceofs, so we end up with ClassCastException instead of ICCE. > In any case, I am not getting your point about "but people can catch it." So > what? People can catch OOME too, and try to parse the output of toString() when > we tell them not to. But that's no reason to make all exceptions "OpaqueError". > So what is your point here? You can catch OOME if you write the code by hand. People are using IDEs and when the IDE is lost or the user have click on the wrong button, catch(Exception) appears. That the reason why we have both IOError and UncheckedIOException in the JDK. >>> Note that throwing an exception from remainder is delayed until the last >>> possible moment. We could have: >>> case Box(Bag(var x)): ... >>> case Box(var x) when x == null: ... >>> and the reasonable treatment is to treat Box(Bag(var x)) as not matching >>> Box(null), even though it is exhuastive on Box>), and therefore fall >>> into the second case on Box(null). Only when we reach the end of the switch, >>> and we haven't matched any cases, do we throw MatchRemainderException. >> I really dislike that idea, it will be a burden in the future each time we want >> to change the implementation. >> I would like the semantics to make no promise about when the error will be >> thrown, the semantics should not be defined if a deconstructors/pattern method >> does a side effect, the same way the stream semantics is not defined if the >> lambda taken as parameter of Stream.map() does a side effect. >> I think the parallel between the pattern matching and a stream in term of >> execution semantics is important here. From the outside, those things are >> monads, they should work the same way. > I think this stems from the same misunderstanding you have about the boundary > between the pattern semantics and the construct semantics. I'm going to > test-drive some adjusted language here. > A total pattern is just that -- it matches everything. > Some patterns are considered exhaustive, but not total. A deconstruction pattern > D(E(total)) is one such example; it is exhaustive on D, but does not match > D(null), because matching the nested E(total) requires invoking a deconstructor > in E, and you can't invoke an instance member on a null receiver. Still, we > consider D(E(total)) exhaustive on D, which means it is enough to satisfy > the type checker that you've covered everything. Remainder is just the gap > between exhaustiveness and totality. The gap is due to E(...) not matching null, for me it's a NPE with an error message saying exactly that. > If we have the following switch: > case D(E(Object o)): > case D(var x) when x == null: > the semantics of D(E(Object)) are that *it matches all non-null D, except > D(null)*. So throwing when we evaluate the case would be incorrect; switch asks > the pattern "do you match", and the pattern says "no, I do not." And the > semantics of switch, then, say "then I will keep trying the rest of the cases." What you are saying is that at runtime you need to know if a pattern is total or not, exactly you need to know if was decided to be total at compile, so at runtime you can decide to throw a NPE or not. Furthermore, if at runtime you detect that the total pattern is not total anymore, a ICCE should be raised. > So *when* the error is thrown derives from the semantics of the construct; > switch tries matching with each pattern, until it finds a match or runs out of > patterns. When it runs out of patterns is when it needs to insert a catch-all > to deal with remainder (as we do with enum switch expressions.) The semantics of switch at runtime need to be feed with whenever a pattern was considered as total or not at compile time. R?mi -------------- next part -------------- An HTML attachment was scrubbed... URL: From brian.goetz at oracle.com Fri Feb 18 14:34:45 2022 From: brian.goetz at oracle.com (Brian Goetz) Date: Fri, 18 Feb 2022 09:34:45 -0500 Subject: [External] : Re: Record patterns (and beyond): exceptions In-Reply-To: <756246617.5170118.1645170357069.JavaMail.zimbra@u-pem.fr> References: <84f64bf3-7803-48d3-b94b-8b0640a20864@oracle.com> <1564273027.4805699.1645094718556.JavaMail.zimbra@u-pem.fr> <756246617.5170118.1645170357069.JavaMail.zimbra@u-pem.fr> Message-ID: <559c8d4c-1a32-144c-a719-bd4c1c8cf704@oracle.com> > But this clearly does not fall into ICCE.? ICCE means, basically, > "your classpath is borked"; that things that were known to be true > at compile time are not true at runtime. (Inconsistent separate > compilation is the most common cause.)? But Box(Bag(null)) is not > an artifact of inconsistent separate compilation. > > > I think i've not understood the problem correctly, i was thinking the > error was due to the erasure, Box being erased to Box, the > problem with erasure is that you see the problem late, in case of the > switch after the phase that does instanceofs, so we end up with > ClassCastException instead of ICCE. CCE is not the right thing either.? Let's step back and go over the concepts. We want for the compiler to be able to do type checking that a switch is "total enough" to not require a default clause.? We want this not just because writing a default clause when you think you've got things covered is annoying, but also, because once you have a default clause, you've given up on getting any better type checking for totality.? In a switch over enum X {A, B}, having only cases for A and B means that, when someone adds C later, you'll find out about it, rather than sweeping it under the rug.? Sealed class hierarchies have the same issues as enums; the possibility of novel values due to separate compilation.? So far, all of these could be described by ICCE (and they are, currently.) We've already talked for several lifetimes about null; switches that reject null do so with NPE.? That also makes sense.? We had hoped that this covered the weird values that might leak out of otherwise-exhaustive switches, but that was wishful thinking. Having nested deconstruction patterns introduces an additional layer of weirdness.? Suppose we have ??? sealed interface A permits X, Y { } ??? Box box; ??? switch (box) { ??????? case Box(X x): ??????? case Box(Y y): ??? } This should be exhaustive, but we have to deal with two additional bad values: Box(null), which is neither a Box(A) or a Box(B), and Box(C), for a novel subtype C.? We don't want to disturb the user to deal with these by making them have a default clause. So we define exhaustiveness separately from totality, and remainder is the difference.? (We can constructively characterize the upper bound on remainder.)? And we can introduce a throwing default, as we did with expression switches over enums.? But what should it throw? The obvious but naive answer is "well, Box(null) should throw NPE, and Box(C) should throw ICCE."? But only a few minutes thinking shows this to be misleading, expensive, and arbitrary.? When we encountered Box(null), it was not because anyone tried to dereference a null, so throwing NPE is misleading.? If the shape of the remainder is complicated, this means generating tons of low-value, compiler-generated boilerplate to differentiate Box(Bag(null)) from Box(Container()).? That's expensive.? And, what about Foo(null, C)?? Then we have to arbitrarily pick one.? It's a silly scheme. So the logical thing to do is to say that these things fall into a different category from NPE and ICCE, which is that they are remainder, which gets its own label. > > In any case, I am not getting your point about "but people can > catch it."? So what?? People can catch OOME too, and try to parse > the output of toString() when we tell them not to. But that's no > reason to make all exceptions "OpaqueError". So what is your point > here? > > > You can catch OOME if you write the code by hand. People are using > IDEs and when the IDE is lost or the user have click on the wrong > button, catch(Exception) appears. > That the reason why we have both IOError and UncheckedIOException in > the JDK. I'm still not getting your point. > > Some patterns are considered exhaustive, but not total.? A > deconstruction pattern D(E(total)) is one such example; it is > exhaustive on D, but does not match D(null), because matching the > nested E(total) requires invoking a deconstructor in E, and you > can't invoke an instance member on a null receiver.? Still, we > consider D(E(total)) exhaustive on D, which means it is enough > to satisfy the type checker that you've covered everything. > Remainder is just the gap between exhaustiveness and totality. > > > The gap is due to E(...) not matching null, for me it's a NPE with an > error message saying exactly that. See above -- this is (a) NOT about dereferencing a null; it's about a value outside the set of match values, (b) the scheme involving NPE does not scale, and (c) will eventually force us to silly arbitrary choices. > What you are saying is that at runtime you need to know if a pattern > is total or not, exactly you need to know if was decided to be total > at compile, so at runtime you can decide to throw a NPE or not. > Furthermore, if at runtime you detect that the total pattern is not > total anymore, a ICCE should be raised. No, what I'm saying is that totality and exhaustiveness are related, but separate, concepts, and these do not stem from NPE or ICCE, that this is a fundamental thing about switch exhaustiveness (and later, same for let/bind) that needs to be captured in the language. -------------- next part -------------- An HTML attachment was scrubbed... URL: From forax at univ-mlv.fr Fri Feb 18 15:07:13 2022 From: forax at univ-mlv.fr (forax at univ-mlv.fr) Date: Fri, 18 Feb 2022 16:07:13 +0100 (CET) Subject: [External] : Re: Record patterns (and beyond): exceptions In-Reply-To: <559c8d4c-1a32-144c-a719-bd4c1c8cf704@oracle.com> References: <84f64bf3-7803-48d3-b94b-8b0640a20864@oracle.com> <1564273027.4805699.1645094718556.JavaMail.zimbra@u-pem.fr> <756246617.5170118.1645170357069.JavaMail.zimbra@u-pem.fr> <559c8d4c-1a32-144c-a719-bd4c1c8cf704@oracle.com> Message-ID: <1315176671.5454006.1645196833743.JavaMail.zimbra@u-pem.fr> > From: "Brian Goetz" > To: "Remi Forax" > Cc: "amber-spec-experts" > Sent: Friday, February 18, 2022 3:34:45 PM > Subject: Re: [External] : Re: Record patterns (and beyond): exceptions >>> But this clearly does not fall into ICCE. ICCE means, basically, "your classpath >>> is borked"; that things that were known to be true at compile time are not true >>> at runtime. (Inconsistent separate compilation is the most common cause.) But >>> Box(Bag(null)) is not an artifact of inconsistent separate compilation. >> I think i've not understood the problem correctly, i was thinking the error was >> due to the erasure, Box being erased to Box, the problem with erasure is >> that you see the problem late, in case of the switch after the phase that does >> instanceofs, so we end up with ClassCastException instead of ICCE. > CCE is not the right thing either. Let's step back and go over the concepts. > We want for the compiler to be able to do type checking that a switch is "total > enough" to not require a default clause. We want this not just because writing > a default clause when you think you've got things covered is annoying, but > also, because once you have a default clause, you've given up on getting any > better type checking for totality. In a switch over enum X {A, B}, having only > cases for A and B means that, when someone adds C later, you'll find out about > it, rather than sweeping it under the rug. Sealed class hierarchies have the > same issues as enums; the possibility of novel values due to separate > compilation. So far, all of these could be described by ICCE (and they are, > currently.) > We've already talked for several lifetimes about null; switches that reject null > do so with NPE. That also makes sense. We had hoped that this covered the weird > values that might leak out of otherwise-exhaustive switches, but that was > wishful thinking. > Having nested deconstruction patterns introduces an additional layer of > weirdness. Suppose we have > sealed interface A permits X, Y { } > Box box; > switch (box) { > case Box(X x): > case Box(Y y): > } > This should be exhaustive, but we have to deal with two additional bad values: > Box(null), which is neither a Box(A) or a Box(B), and Box(C), for a novel > subtype C. We don't want to disturb the user to deal with these by making them > have a default clause. > So we define exhaustiveness separately from totality, and remainder is the > difference. (We can constructively characterize the upper bound on remainder.) > And we can introduce a throwing default, as we did with expression switches > over enums. But what should it throw? > The obvious but naive answer is "well, Box(null) should throw NPE, and Box(C) > should throw ICCE." But only a few minutes thinking shows this to be > misleading, expensive, and arbitrary. When we encountered Box(null), it was not > because anyone tried to dereference a null, so throwing NPE is misleading. A NPE is not a problem if (the big if) the error message is "null neither match Box(X) nor Box(Y)" > If the shape of the remainder is complicated, this means generating tons of > low-value, compiler-generated boilerplate to differentiate Box(Bag(null)) from > Box(Container()). That's expensive. And, what about Foo(null, C)? Then > we have to arbitrarily pick one. It's a silly scheme. We already talked about that, the shape of the remainder is complex if you want to generate all branches at compile time, it's not an issue if you generate the branches at runtime, because you can generate them lazily. For some checks, they can only be done at runtime anyway, like does this pattern is still total ? About Foo(null, C), i suppose you mean a case where you have both a null that need to be deconstructed and a new subtype, the solution is to go left to right, like usual in Java. > So the logical thing to do is to say that these things fall into a different > category from NPE and ICCE, which is that they are remainder, which gets its > own label. Nope, as a user i want a real error message, not something saying nope, sorry too complex, i bailout. [...] >>> Some patterns are considered exhaustive, but not total. A deconstruction pattern >>> D(E(total)) is one such example; it is exhaustive on D, but does not match >>> D(null), because matching the nested E(total) requires invoking a deconstructor >>> in E, and you can't invoke an instance member on a null receiver. Still, we >>> consider D(E(total)) exhaustive on D, which means it is enough to satisfy >>> the type checker that you've covered everything. Remainder is just the gap >>> between exhaustiveness and totality. >> The gap is due to E(...) not matching null, for me it's a NPE with an error >> message saying exactly that. > See above -- this is (a) NOT about dereferencing a null; it's about a value > outside the set of match values, (b) the scheme involving NPE does not scale, > and (c) will eventually force us to silly arbitrary choices. It scales if you don't try to generates all the branches at compile time but only generate the one you need at runtime. Like JEP 358 (Helpful NPE) at the point you detect an error, you can take a look at all the patterns that may match and generate a helpful error message. >> What you are saying is that at runtime you need to know if a pattern is total or >> not, exactly you need to know if was decided to be total at compile, so at >> runtime you can decide to throw a NPE or not. >> Furthermore, if at runtime you detect that the total pattern is not total >> anymore, a ICCE should be raised. > No, what I'm saying is that totality and exhaustiveness are related, but > separate, concepts, and these do not stem from NPE or ICCE, that this is a > fundamental thing about switch exhaustiveness (and later, same for let/bind) > that needs to be captured in the language. I agree that totality and exhaustiveness are separate concept but at runtime if you detect that either exhaustiveness or totality is not true anymore, you can generate the appropriate exception with an helpful error message. R?mi -------------- next part -------------- An HTML attachment was scrubbed... URL: From brian.goetz at oracle.com Fri Feb 18 15:20:33 2022 From: brian.goetz at oracle.com (Brian Goetz) Date: Fri, 18 Feb 2022 10:20:33 -0500 Subject: [External] : Re: Record patterns (and beyond): exceptions In-Reply-To: <1315176671.5454006.1645196833743.JavaMail.zimbra@u-pem.fr> References: <84f64bf3-7803-48d3-b94b-8b0640a20864@oracle.com> <1564273027.4805699.1645094718556.JavaMail.zimbra@u-pem.fr> <756246617.5170118.1645170357069.JavaMail.zimbra@u-pem.fr> <559c8d4c-1a32-144c-a719-bd4c1c8cf704@oracle.com> <1315176671.5454006.1645196833743.JavaMail.zimbra@u-pem.fr> Message-ID: <1979a350-be4b-3342-1098-6779fcbdac09@oracle.com> We're lost in the weeds; I really can't follow what you're on about here, and more replies doesn't seem to be improving it. Since we're rapidly heading towards the danger zones I warned about in: https://mail.openjdk.java.net/pipermail/amber-spec-observers/2020-August/002458.html I think we should prune this sub-thread and give other folks a chance to reply to the main points. On 2/18/2022 10:07 AM, forax at univ-mlv.fr wrote: > > > ------------------------------------------------------------------------ > > *From: *"Brian Goetz" > *To: *"Remi Forax" > *Cc: *"amber-spec-experts" > *Sent: *Friday, February 18, 2022 3:34:45 PM > *Subject: *Re: [External] : Re: Record patterns (and beyond): > exceptions > > > But this clearly does not fall into ICCE.? ICCE means, > basically, "your classpath is borked"; that things that > were known to be true at compile time are not true at > runtime.? (Inconsistent separate compilation is the most > common cause.)? But Box(Bag(null)) is not an artifact of > inconsistent separate compilation. > > > I think i've not understood the problem correctly, i was > thinking the error was due to the erasure, Box being > erased to Box, the problem with erasure is that you see the > problem late, in case of the switch after the phase that does > instanceofs, so we end up with ClassCastException instead of ICCE. > > > CCE is not the right thing either.? Let's step back and go over > the concepts. > > We want for the compiler to be able to do type checking that a > switch is "total enough" to not require a default clause. We want > this not just because writing a default clause when you think > you've got things covered is annoying, but also, because once you > have a default clause, you've given up on getting any better type > checking for totality.? In a switch over enum X {A, B}, having > only cases for A and B means that, when someone adds C later, > you'll find out about it, rather than sweeping it under the rug.? > Sealed class hierarchies have the same issues as enums; the > possibility of novel values due to separate compilation.? So far, > all of these could be described by ICCE (and they are, currently.) > > We've already talked for several lifetimes about null; switches > that reject null do so with NPE.? That also makes sense.? We had > hoped that this covered the weird values that might leak out of > otherwise-exhaustive switches, but that was wishful thinking. > > Having nested deconstruction patterns introduces an additional > layer of weirdness.? Suppose we have > > ??? sealed interface A permits X, Y { } > ??? Box box; > > ??? switch (box) { > ??????? case Box(X x): > ??????? case Box(Y y): > ??? } > > This should be exhaustive, but we have to deal with two additional > bad values: Box(null), which is neither a Box(A) or a Box(B), and > Box(C), for a novel subtype C.? We don't want to disturb the user > to deal with these by making them have a default clause. > > So we define exhaustiveness separately from totality, and > remainder is the difference.? (We can constructively characterize > the upper bound on remainder.)? And we can introduce a throwing > default, as we did with expression switches over enums.? But what > should it throw? > > The obvious but naive answer is "well, Box(null) should throw NPE, > and Box(C) should throw ICCE."? But only a few minutes thinking > shows this to be misleading, expensive, and arbitrary.? When we > encountered Box(null), it was not because anyone tried to > dereference a null, so throwing NPE is misleading. > > > A NPE is not a problem if (the big if) the error message is "null > neither match Box(X) nor Box(Y)" > > If the shape of the remainder is complicated, this means > generating tons of low-value, compiler-generated boilerplate to > differentiate Box(Bag(null)) from Box(Container()).? That's > expensive.? And, what about Foo(null, C)?? Then we have to > arbitrarily pick one. It's a silly scheme. > > > We already talked about that, the shape of the remainder is complex if > you want to generate all branches at compile time, it's not an issue > if you generate the branches at runtime, because you can generate them > lazily. > For some checks, they can only be done at runtime anyway, like does > this pattern is still total ? > > About Foo(null, C), i suppose you mean a case where you have both a > null that need to be deconstructed and a new subtype, the solution is > to go left to right, like usual in Java. > > > > So the logical thing to do is to say that these things fall into a > different category from NPE and ICCE, which is that they are > remainder, which gets its own label. > > > Nope, as a user i want a real error message, not something saying > nope, sorry too complex, i bailout. > > [...] > > > > > > Some patterns are considered exhaustive, but not total.? A > deconstruction pattern D(E(total)) is one such example; it > is exhaustive on D, but does not match D(null), because > matching the nested E(total) requires invoking a > deconstructor in E, and you can't invoke an instance > member on a null receiver. Still, we consider D(E(total)) > exhaustive on D, which means it is enough to satisfy > the type checker that you've covered everything. Remainder > is just the gap between exhaustiveness and totality. > > > The gap is due to E(...) not matching null, for me it's a NPE > with an error message saying exactly that. > > > See above -- this is (a) NOT about dereferencing a null; it's > about a value outside the set of match values, (b) the scheme > involving NPE does not scale, and (c) will eventually force us to > silly arbitrary choices. > > > It scales if you don't try to generates all the branches at compile > time but only generate the one you need at runtime. > Like JEP 358 (Helpful NPE) at the point you detect an error, you can > take a look at all the patterns that may match and generate a helpful > error message. > > > > What you are saying is that at runtime you need to know if a > pattern is total or not, exactly you need to know if was > decided to be total at compile, so at runtime you can decide > to throw a NPE or not. > Furthermore, if at runtime you detect that the total pattern > is not total anymore, a ICCE should be raised. > > > No, what I'm saying is that totality and exhaustiveness are > related, but separate, concepts, and these do not stem from NPE or > ICCE, that this is a fundamental thing about switch exhaustiveness > (and later, same for let/bind) that needs to be captured in the > language. > > > I agree that totality and exhaustiveness are separate concept but at > runtime if you detect that either exhaustiveness or totality is not > true anymore, you can generate the appropriate exception with an > helpful error message. > > R?mi > -------------- next part -------------- An HTML attachment was scrubbed... URL: From brian.goetz at oracle.com Fri Feb 25 21:45:44 2022 From: brian.goetz at oracle.com (Brian Goetz) Date: Fri, 25 Feb 2022 16:45:44 -0500 Subject: Primitive type patterns Message-ID: As a consequence of doing record patterns, we also grapple with primitive type patterns. Until now, we've only supported reference type patterns, which are simple: ?- A reference type pattern `T t` is applicable to a match target of type M if M can be cast to T without an unchecked warning. ?- A reference type pattern `T t` covers a match type M iff M <: T ?- A reference type pattern `T t` matches a value m of type M if M <: T || m instanceof T Two of these three characterizations are static computations (applicability and coverage); the third is a runtime test (matching).? For each kind of pattern, we have to define all three of these. #### Primitive type patterns in records Record patterns necessitate the ability to write type patterns for any type that can be a record component.? If we have: ??? record IntBox(int i) { } then we want to be able to write: ??? case IntBox(int i): which means we need to be able to express type patterns for primitive types. #### Relationship with assignment context There is another constraint on primitive type patterns: the let/bind statement coming down the road.? Because a type pattern looks (not accidentally) like a local variable declaration, a let/bind we will want to align the semantics of "local variable declaration with initializer" and "let/bind with total type pattern".? Concretely: ??? let String s = "foo"; is a pattern match against the (total) pattern `String s`, which introduces `s` into the remainder of the block.? Since let/bind is a generalization of local variable declaration with initialization, let/bind should align with locals where the two can express the same thing.? This means that the set of conversions allowed in assignment context (JLS 5.2) should also be supported by type patterns. Of the conversions supported by 5.2, the only one that applies when both the initializer and local variable are of reference type is "widening reference", which the above match semantics (`T t` matches `m` when `M <: T`) support.? So we need to fill in the other three boxes of the 2x2 matrix of { ref, primitive } x { ref, primitive }. The conversions allowed in assignment context are: ?- Widening primitive -- `long l = anInt` ?- Narrowing primitive -- `byte b = 0L` (only applies to constants on RHS) ?- Widening reference -- `Object o = aString` ?- Widening reference + unbox -- where ``, `int i = t` ?- Widening reference + unbox + widening primitive -- `long l = t` ?- Unboxing -- `int i = anInteger` (may NPE) ?- Unboxing + widening primitive -- `long l = anInteger` (may NPE) ?- Boxing -- `Integer i = anInt` ?- Boxing + widening reference -- `Object o = anInt` #### Boxing and unboxing Suppose our match target is a box type, such as: ??? record IntegerBox(Integer i) { } Clearly we can match it with: ??? case IntegerBox(Integer i): If we want to align with assignment context, and support things like ??? let int i = anInteger (and if we didn't, this would likely be seen as a gratuitous gap between let/bind and local declaration), we need for `int i` to be applicable to `Integer`: ??? case IntegerBox(int i): There is one value of `Integer` that, when we try to unbox, causes trouble: null.? As of Java 5 when switching on wrapper types, we unbox eagerly, throwing NPE if the target is null. But pattern matching is conditional.? If we have: ??? record Box(T t) { } ??? Box b; ??? ... ??? case Box(String s): when we encounter values of Object that are not instances of String, we just don't match.? For unboxing, it should be the same; `int x` matches all non-null instances of `Integer`: ??? case IntegerBox(int i): Because `int i` matches all instances of Integer other than null, it is reasonable to say that `int i` _covers_ Integer, with remainder null, just like: ??? Box> bbs; ??? switch (bbs) { ??????? case Box(Box(String s)): ... ??? } covers the match target, with remainder Box(null).? When confronted with Box(null), the attempt to match the case doesn't throw, it just doesn't match; when we run out of cases, the switch can throw a last-ditch exception.? The same applies when unboxing would NPE. In the other direction, a primitive can always be boxed to its wrapper (or a supertype), meaning `Integer x` is applicable to, and covers, int, short, char, and byte. #### Primitive widening and narrowing The pattern match equivalent of primitive widening is: ??? let long l = anInt; or ??? case IntBox(long l): (When we get to dtor patterns, we will have to deal with overload selection, but for record patterns, there is one canonical dtor.)? This seems uncontroversial, just as allowing `Object o` to match a `String` target. Primitive narrowing is less obvious, but there's a strong argument for generalizing primitive narrowing in pattern matching beyond constants.? We already have to deal with ??? let byte b = 0L; via primitive narrowing, but pattern matching is a conditional construct, and there's an obvious way to extend this.? Observe that when matching `Box(String s)` against a `Box`, this is equivalent (because the nested pattern is not total) to matching the target to `Box(var alpha)` and then further matching alpha to `String s`.? So if we have the primitive equivalent: ??? case IntBox(short s) then this should be the same as matching to `IntBox(var alpha)` (which is an int) and then matching that int to `short s`.? The semantics of such a match are a dynamic range check, which is analogous to a dynamic `instanceof` check. #### Applicability We can fill out the other three quadrants now.? We start with applicability, which is a static check to see if the pattern is even allowed against the target type.? The clauses that are richer than allowed in assignment context (and which we could consider deferring) are written in brackets. ?- A primitive type pattern `P p` should be applicable to a primitive target `q : Q` if P == Q, Q can be widened to P, q is a constant and Q can be narrowed to P [ or Q can be narrowed to P.? ] ?- A primitive type pattern `P p` should be applicable to a reference target T if T unboxes to P, or T unboxes to a primitive type that can be widened to P [ or if T unboxes to a primitive type that can be narrowed to P. ] ?- A reference type pattern `T t` should be applicable to a primitive target P if P boxes to a type that is cast-convertible to T Note that we're _not_ trying to treat `case 0` as matching all of Integer 0, Short 0, and Long 0. #### Coverage We need to add corresponding rules for coverage (exhaustiveness). ?- A primitive type pattern `P p` covers any primitive type Q which can be widened to P; ?- A primitive type pattern `P p` covers P's box type (with remainder null); ?- A reference type pattern `T t` covers a primitive type P if P's box type is a subtype of T. #### Matching ?- A primitive type pattern `P p` matches a primitive value q : Q if P == Q, Q can be widened to P, if q is a constant in the range of P [ or if q is in the range of P ] ?- A primitive type pattern `P p` matches a reference value t : T by unboxing t (and optionally widening to P) when t != null ?- A reference type pattern `T t` matches a primitive value `p : P` when box(p) instanceof T (always true given applicability rule) #### Comparing with assignment context Let's go through our table of assignment conversions, flipping `T t = e` around to `e instanceof T t` ?- Widening primitive -- `anInt instanceof long l`. Applicable because int can be widened to long.? Matches always. ?- Narrowing primitive -- `0L instanceof byte b`. Applicable because OL is a constant and long can be narrowed to byte.? Matches always. ?- Widening reference -- `aString instanceof Object o`. Applicable because String can be cast to Object without an unchecked warning.? Matches always. ?- Widening reference + unbox -- where ``, `t instanceof int i`.? Applicable because T unboxes to int. ?- Widening reference + unbox + widening primitive -- `t instanceof long l`.? Applicable because T unboxes to int, and int can be widened to long. ?- Unboxing -- `anInteger instanceof int i`.? Applicable because Integer unboxes to int.? Will match all values except null. ?- Unboxing + widening primitive -- `anInteger instanceof long l`.? Applicable because Integer unboxes to int, and int can be widened to long.? Will match all values except null. ?- Boxing -- `anInt instanceof Integer i`.? Applicable because int boxes to Integer. ?- Boxing + widening reference -- `anInt instanceof Object i`.? Applicable because int boxes to Integer, which is cast-convertible to Object.? Matches when box(anInt) instanceof Object. There is an additional case which our rules cover which are not allowed in assignment context: ?- Narrowing without constants -- `aLong instanceof int i`.? Allowed by bracketed rules.? Matches when the long is in the range of an int. #### Looking to Valhalla When we get to primitive classes, the rules about boxing and unboxing will translate to the widening/narrowing conversions between P and P.ref. -------------- next part -------------- An HTML attachment was scrubbed... URL: From forax at univ-mlv.fr Sat Feb 26 12:02:29 2022 From: forax at univ-mlv.fr (Remi Forax) Date: Sat, 26 Feb 2022 13:02:29 +0100 (CET) Subject: Primitive type patterns In-Reply-To: References: Message-ID: <1085786642.8814171.1645876949816.JavaMail.zimbra@u-pem.fr> > From: "Brian Goetz" > To: "amber-spec-experts" > Sent: Friday, February 25, 2022 10:45:44 PM > Subject: Primitive type patterns > As a consequence of doing record patterns, we also grapple with primitive type > patterns. Until now, we've only supported reference type patterns, which are > simple: > - A reference type pattern `T t` is applicable to a match target of type M if M > can be cast to T without an unchecked warning. > - A reference type pattern `T t` covers a match type M iff M <: T > - A reference type pattern `T t` matches a value m of type M if M <: T || m > instanceof T > Two of these three characterizations are static computations (applicability and > coverage); the third is a runtime test (matching). For each kind of pattern, we > have to define all three of these. > #### Primitive type patterns in records > Record patterns necessitate the ability to write type patterns for any type that > can be a record component. If we have: > record IntBox(int i) { } > then we want to be able to write: > case IntBox(int i): > which means we need to be able to express type patterns for primitive types. > #### Relationship with assignment context > There is another constraint on primitive type patterns: the let/bind statement > coming down the road. Because a type pattern looks (not accidentally) like a > local variable declaration, a let/bind we will want to align the semantics of > "local variable declaration with initializer" and "let/bind with total type > pattern". Concretely: > let String s = "foo"; > is a pattern match against the (total) pattern `String s`, which introduces `s` > into the remainder of the block. Since let/bind is a generalization of local > variable declaration with initialization, let/bind should align with locals > where the two can express the same thing. This means that the set of > conversions allowed in assignment context (JLS 5.2) should also be supported by > type patterns. > Of the conversions supported by 5.2, the only one that applies when both the > initializer and local variable are of reference type is "widening reference", > which the above match semantics (`T t` matches `m` when `M <: T`) support. So > we need to fill in the other three boxes of the 2x2 matrix of { ref, primitive > } x { ref, primitive }. That's a huge leap, let's take a step back. I see two questions that should be answered first. 1) do we really want pattern in case of assignment/declaration to support assignment conversions ? 2) do we want patterns used by the switch or instanceof to follow the exact same rules as patterns used in assignment/declaration ? For 1, given that we are using pattern to do destructured assignment, we may want to simplify the assignment rules to keep things simple avoid users shooting themselves in the foot with implicit unboxing. With an example, record Box(T value) {} Box box = ... Box<>(int result) = box; // assignment of result may throw a NPE I don't think we have to support that implicit unboxing given that we have a way to ask for an unboxing explicitly (once java.lang.Integer have a de-constructor) Box<>(Integer(int result)) = box; I think we should not jump with the shark too soon here and ask ourselves if we really want assignment conversions in case of destructured assignment. 2) we already know that depending on the context (inside a switch, inside a instanceof, inside an assignment) the rules for pattern are not exactly the same. So we may consider that in the assignment context, assignment conversions apply while for a matching context, simpler rules apply. Given that the syntax for switch reuse '->', i believe we should use the overriding rules (the one we use for lambdas) instead of the assignment rules (the one we use for method reference). And yes, i know that the '->' of switch is not the same as the '->' of lambda, but i don't think we should bother users to intuitively think that the same rules apply. Then the model you propose is too clever for me, the fact that instanceof Point(double x, double y) has a different meaning depending if Point is declared like record Point(double x, double y) { } or like this record Point(Double x, Double y) { } is too much. The semantics of Java around null is already a giant landmine field, we should restraint ourselves to add more null-funny behaviors. regards, R?mi -------------- next part -------------- An HTML attachment was scrubbed... URL: From brian.goetz at oracle.com Sat Feb 26 16:49:08 2022 From: brian.goetz at oracle.com (Brian Goetz) Date: Sat, 26 Feb 2022 11:49:08 -0500 Subject: [External] : Re: Primitive type patterns In-Reply-To: <1085786642.8814171.1645876949816.JavaMail.zimbra@u-pem.fr> References: <1085786642.8814171.1645876949816.JavaMail.zimbra@u-pem.fr> Message-ID: <697fca40-9579-90df-1f9f-57993ad1bbd2@oracle.com> > > #### Relationship with assignment context > > > That's a huge leap, let's take a step back. > > I see two questions that should be answered first. > 1) do we really want pattern in case of assignment/declaration to > support assignment conversions ? > 2) do we want patterns used by the switch or instanceof to follow the > exact same rules as patterns used in assignment/declaration ? I agree we should take a step back, but let's take a step farther -- because I want to make an even bigger leap that you think :) Stepping way far back .... in the beginning ... Java had reference types with subtyping, and eight primitive types.? Which raises an immediate question: what types can be assigned to what?? Java chose a sensible guideline; assignment should be allowed if the value set on the left is "bigger" than that on the right.? This gives us String => Object, int => long, int => double, etc.? (At this point, note that we've gone beyond strict value set inclusion; an int is *not* a floating point number, but we chose (reasonably) to do the conversion because we can *embed* the ints in the value set of double.?? Java was already appealing to the notion of embedding-projection pair even then, in assignment conversions; assignment from A to B is OK if we have an embedding of A into B.) On the other hand, Java won't let you assign long => int, because it might be a lossy conversion.? To opt into the loss, you have to cast, which acknowledges that the conversion may be information-losing.? Except!? If you can prove the conversion isn't information losing (because the thing on the right is a compile-time constant), then its OK, because we know its safe.? JLS Ch5 had its share of ad-hoc-seeming complexity, but mostly stayed in its corner until you called it, and the rules all seemed justifiable. Then we added autoboxing.? And boxing is not problematic; int embeds into Integer.? So the conversion from int => Integer is fine. (It added more complexity to overload selection, brought in strict and loose conversion contexts, and we're still paying when methods like remove(int) merge with remove(T), but OK.)? But the other direction is problematic; there is one value of Integer that doesn't correspond to any value of int, which is our favorite value, null. The decision made at the time was to allow the conversion from Integer => int, and throw on null. This was again a justifiable choice, and comes from the fact that the mapping from Integer to int is a _projection_, not an embedding.? It was decided (reasonably, but we could have gone the other way too) that null was a "silly" enough value to justify not requiring a cast, and throwing if the silly value comes up.? We could have required a cast from Integer to int, as we do from long to int, and I can imagine the discussion about why that was not chosen. Having set the stage, one can see all the concepts in pattern matching dancing on it, just with different names. Whether we can assign T to U with or without a cast, is something we needed a static rule for.? So we took the set of type pairs (T, U) for which the pattern `T t` is strictly total on U, and said "these are the conversions allowed in assignment context" (with a special rule for when the target is an integer constant.) When we got to autoboxing, we made a subjective call that `int x` should be "total enough" on `Integer` that we're willing to throw in the one place it's not.? That's exactly the concept of "P is exhaustive, but not total, on T" (i.e., there is a non-empty remainder.)? All of this has happened before.? All of this will happen again. So the bigger leap I've got in mind is: what would James et al have done, had they had pattern matching from day one?? I believe that: ?- T t = u would be allowed if `T t` is exhaustive on the static type of u; ?- If there is remainder, assignment can throw (preserving the invariant that if the assignment completes normally, something was assigned). So it's not that I want to align assignment with pattern matching because we've got a syntactic construct on the whiteboard that operates by pattern matching but happens to looks like assignment; it's because assignment *is* a constrained case of pattern matching.? We've found the missing primitive, and I want to put it under the edifice.? If we define pattern matching correctly, we could rewrite JLS 5.2 entirely in terms of pattern matching (whether we want to actually rewrite it or not, that's a separate story.) The great thing about pattern matching as a generalization of assignment is that it takes pressure off the one-size-fits-all ruleset.? You can write: ??? int x = anInteger but it might throw NPE.? In many cases, users are fine with that. But by interpreting it as a pattern, when we get into more flexible constructs, we don't *have* to throw eagerly.? If the user said: ??? if (anInteger instanceof int x) { ... } then we match the pattern on everything but null, and don't match on null, and since instanceof is a conditional construct, no one needs to throw at all.? And if the user said: ??? switch (anInteger) { ??????? case int x: ... ??????? // I can have more cases ??? } the `int x` case is taken for all values other than null, and the user has a choice to put more patterns that will catch up the remainder and act on them, or not; and if the user chooses "not", the remainder is implicitly rejected by switches handling of "exhaustive with remainder", but the timing of such is moved later, after the user has had as many bites at the apple as desired before we throw.? The rules about assignment are the way they are because there's no statically trackable side-channel for "was it a good match".? Now there is; let's use it. So, enough philosophy; on to the specific objections. > For 1, given that we are using pattern to do destructured assignment, > we may want to simplify the assignment rules to keep things simple > avoid users shooting themselves in the foot with implicit unboxing. > With an example, > ? record Box(T value) {} > ? Box box = ... > ? Box<>(int result) = box;?? // assignment of result may throw a NPE I assume you mean "let Box... = box".?? Assuming so, let's analyze the above. The pattern Box(P) is exhaustive on Box if P is exhaustive on Integer.? If we say that `int result` is exhaustive on Integer (which I'm proposing), then the remainder of `Box(int result)` will be { null, Box(null) }.? The pattern won't match the remainder (but *matching* does not throw), but the let/bind construct says "OK, if there was remainder, throw" (unless there's an else clause, yada yada.)? So yes, the above would throw (I don't think it should throw NPE, but we're going to discuss that in a separate thread) not because of the pattern -- pattern matching *never* throws -- but because the let construct wants exhaustiveness, and accepts that some patterns have remainder, and makes up the difference by completing abruptly.? It's just like: ??? Box> bbs = new Box(null); ??? let Box(Box(String s)) = bbs; Here, Box(null) is in the remainder of Box(Box(String s)) on Box>, so the match fails, and the construct throws.? Unboxing is not really any different, and I wouldn't want to treat them differently (and I worry that yet again, there's some "anti-null bias" going on.)? In both cases, we have a nested pattern that is exhaustive but has non-empty remainder. > I don't think we have to support that implicit unboxing given that we > have a way to ask for an unboxing explicitly (once java.lang.Integer > have a de-constructor) > > ? Box<>(Integer(int result)) = box; This is just moving the remainder somewhere else; Box(null) is in the remainder of Box(Integer(BLAH)), since deconstructing the Integer requires invoking a deconstructor whose receiver would be null.? I think what you're reaching for here is "the user should have to explicitly indicate that the conversion might not succeed", which would be analogous to "the user should have to cast Integer to int".? But we've already made that decision; we don't require such a cast. > I think we should not jump with the shark too soon here and ask > ourselves if we really want assignment conversions in case of > destructured assignment. See above; I think this is the wrong question.? It is not a matter of "do we want assignment conversions in destructuring", it is "do we want to be able to *derive* the assignment conversions from pattern matching." > 2) we already know that depending on the context (inside a switch, > inside a instanceof, inside an assignment) the rules for pattern are > not exactly the same. OMG Remi, would you please stop repeating this incorrect claim.? The rules for pattern matching are exactly the same across contexts; the differences are that the contexts get to choose when to try to match, and what to do if nothing matches. > So we may consider that in the assignment context, assignment > conversions apply while for a matching context, simpler rules apply. We could of course say that; we could say that `int x` is simply *not applicable* to a target of type Integer.? We can discuss that, but I don't think its a simplification, though; I think its actually *more* complexity because it's yet another context with yet another subtly different set of rules.? One obvious consequence of that restriction would be that users cannot refactor ??? Foo f = e to ??? let Foo f = e to ??? if (e instanceof Foo f) { ... } for yet more accidental reasons.? Is this really making things simpler? > Then the model you propose is too clever for me, the fact that > ? instanceof Point(double x, double y) > has a different meaning depending if Point is declared like > ? record Point(double x, double y) { } > or like this > ? record Point(Double x, Double y) { } > is too much. If this is your concern, then I think you are objecting to something much more fundamental than the semantics of primitive patterns; you're objecting to the concept of nesting partial patterns entirely.? Because you can make the same objection (and if I recall correctly, you have) when: ???? record Point(Number x, Number y) { } and you do ???? case Point(Number x, Number y) { } vs ???? case Point(Integer x, Integer y) { } In the case of `Point(Number x, Number y)`, it would be absurd to NPE when x==null; there's nothing in the language that says record components may not be null.? But in the case of Point(Integer x, Integer y) -- where we're nesting partial patterns inside the Point deconstruction pattern -- it would be similarly absurd to match x == null.? (We've been through this, please let's not rehash this.)? So I think your objection is not about null, but that you simply don't like that we have the same syntax for nesting a *total* pattern (Number x), which means "don't do anything, I'm just declaring a variable to receive the component", and nesting a partial pattern (Integer x), which means "make sure this sub-pattern matches before matching the composite."? That is to say, maybe you'd prefer that ??? case Point(Integer x, Integer y) be a type error, and require the user say something like (to pick a random syntax) ??? case Point(match Integer x, match Integer y) so that the user is slapped in the face with the partiality of the nested pattern. This is a valid concern -- sometimes people refer to this as action-at-a-distance -- that to know what a pattern means, you may have to peek at its declaration.? But I do not think such a choice would age very well; it is a sort of "training wheels" choice, which gives us some confidence when we are first learning to pattern match, and thereafter are just new sources of friction. In any case, your objection seems more fundamental than whether `int x` should match Integer; it seems that you view pattern composition as inherently confusing, and therefore you want to reach for warning signs or seat belts.? But non-compositional deconstruction patterns would be pretty weak. > The semantics of Java around null is already a giant landmine field, > we should restraint ourselves to add more null-funny behaviors. I think we agree on the goal, but I think we may disagree on what "adding more null-funny behaviors" means.? What you describe as "disallow this case for simplicity", I see as "add a new null-funny behavior." Your concerns are valid, and we should continue to discuss, but bear in mind that I think they mostly proceed from two places where we may continue to disagree: ?- You are generally much more inclined to say "if it might be null, disallow it / throw eagerly" than I am.? In general, I prefer to let the nulls flow until they hit a point where they can clearly flow no further, rather than introduce null gates into the middle of computations, because null gates are impediments to composition and refactoring. ?- You are viewing pattern matching as the "new thing", and trying to limit it to the cases where you're sure that users who are unfamiliar with it (which is almost all of them) will have a good initial experience.? (This is sort of a semantic analogue of Stroustrup's rule.)? But I believe those limitations, in the long run, will lead to a more complex language and a worse long-term experience.? I want to optimize for where we are going, which is that there is one set of rules for patterns people can reason about, even if they are a little complicated-seeming at first, rather than an ever-growing bag of individually "simple" restrictions. -------------- next part -------------- An HTML attachment was scrubbed... URL: From john.r.rose at oracle.com Sun Feb 27 05:05:14 2022 From: john.r.rose at oracle.com (John Rose) Date: Sat, 26 Feb 2022 21:05:14 -0800 Subject: [External] : Re: Primitive type patterns In-Reply-To: <697fca40-9579-90df-1f9f-57993ad1bbd2@oracle.com> References: <1085786642.8814171.1645876949816.JavaMail.zimbra@u-pem.fr> <697fca40-9579-90df-1f9f-57993ad1bbd2@oracle.com> Message-ID: <6D1E44F3-7AF3-447F-A656-41AB9A6F293B@oracle.com> On 26 Feb 2022, at 8:49, Brian Goetz wrote: > ?I think they mostly proceed from two places where we may continue > to disagree: > > ?- You are generally much more inclined to say "if it might be null, > disallow it / throw eagerly" than I am.? In general, I prefer to let > the nulls flow until they hit a point where they can clearly flow no > further, rather than introduce null gates into the middle of > computations, because null gates are impediments to composition and > refactoring. Now added to the lexicon: ?null gates?. Here?s a slogan to go with it: ?No new null gates.? It?s logically the same as ?Let the nulls flow [until they really can?t]?. > ?- You are viewing pattern matching as the "new thing", and trying to > limit it to the cases where you're sure that users who are unfamiliar > with it (which is almost all of them) will have a good initial > experience.? (This is sort of a semantic analogue of Stroustrup's > rule.)? But I believe those limitations, in the long run, will lead > to a more complex language and a worse long-term experience.? I want > to optimize for where we are going, which is that there is one set of > rules for patterns people can reason about, even if they are a little > complicated-seeming at first, rather than an ever-growing bag of > individually "simple" restrictions. I buy this argument too: A rationalizing retcon for the existing assignment conversion rules will reduce the overall cost of adding patterns. There might be a transient cost to attaching pattern conversions at the deepest level of the language, compared to making patterns into pure sugar bolted on the side. But in the end I?d rather learn something that *does* have deep connections to the language. -------------- next part -------------- An HTML attachment was scrubbed... URL: From forax at univ-mlv.fr Mon Feb 28 12:50:03 2022 From: forax at univ-mlv.fr (forax at univ-mlv.fr) Date: Mon, 28 Feb 2022 13:50:03 +0100 (CET) Subject: [External] : Re: Primitive type patterns In-Reply-To: <697fca40-9579-90df-1f9f-57993ad1bbd2@oracle.com> References: <1085786642.8814171.1645876949816.JavaMail.zimbra@u-pem.fr> <697fca40-9579-90df-1f9f-57993ad1bbd2@oracle.com> Message-ID: <1874101248.9190368.1646052603982.JavaMail.zimbra@u-pem.fr> > From: "Brian Goetz" > To: "Remi Forax" > Cc: "amber-spec-experts" > Sent: Saturday, February 26, 2022 5:49:08 PM > Subject: Re: [External] : Re: Primitive type patterns >>> #### Relationship with assignment context >> That's a huge leap, let's take a step back. >> I see two questions that should be answered first. >> 1) do we really want pattern in case of assignment/declaration to support >> assignment conversions ? >> 2) do we want patterns used by the switch or instanceof to follow the exact same >> rules as patterns used in assignment/declaration ? [...] >> 2) we already know that depending on the context (inside a switch, inside a >> instanceof, inside an assignment) the rules for pattern are not exactly the >> same. >> So we may consider that in the assignment context, assignment conversions apply >> while for a matching context, simpler rules apply. > We could of course say that; we could say that `int x` is simply *not > applicable* to a target of type Integer. We can discuss that, but I don't think > its a simplification, though; I think its actually *more* complexity because > it's yet another context with yet another subtly different set of rules. One > obvious consequence of that restriction would be that users cannot refactor > Foo f = e > to > let Foo f = e > to > if (e instanceof Foo f) { ... } > for yet more accidental reasons. Is this really making things simpler? >> Then the model you propose is too clever for me, the fact that >> instanceof Point(double x, double y) >> has a different meaning depending if Point is declared like >> record Point(double x, double y) { } >> or like this >> record Point(Double x, Double y) { } >> is too much. > If this is your concern, then I think you are objecting to something much more > fundamental than the semantics of primitive patterns; you're objecting to the > concept of nesting partial patterns entirely. Nope, i'm saying that inside a pattern if we let the unboxing to be possible with the semantics that if the value is null, instead of throwing a NPE if it does not match, we are introducing the equivalent of the null-safe operator of Groovy (the elvis operator), something we should not do (and that was rejected in the past, i think as part of Coin or Coin 2). let A(B(C c)) = a else null; is equivalent to var result = a.?getB().?getC(); Maybe it means that we should allow unboxing with the semantics that it throw a NPE i.e the exact semantics of the assignment conversion instead of disallowing unboxing as i proposed, I don't know, but I really dislike the semantics you are proposing here. R?mi -------------- next part -------------- An HTML attachment was scrubbed... URL: From forax at univ-mlv.fr Mon Feb 28 12:52:10 2022 From: forax at univ-mlv.fr (forax at univ-mlv.fr) Date: Mon, 28 Feb 2022 13:52:10 +0100 (CET) Subject: [External] : Re: Primitive type patterns In-Reply-To: <697fca40-9579-90df-1f9f-57993ad1bbd2@oracle.com> References: <1085786642.8814171.1645876949816.JavaMail.zimbra@u-pem.fr> <697fca40-9579-90df-1f9f-57993ad1bbd2@oracle.com> Message-ID: <1631334459.9191969.1646052730183.JavaMail.zimbra@u-pem.fr> From: "Brian Goetz" To: "Remi Forax" Cc: "amber-spec-experts" Sent: Saturday, February 26, 2022 5:49:08 PM Subject: Re: [External] : Re: Primitive type patterns BQ_BEGIN BQ_BEGIN BQ_BEGIN #### Relationship with assignment context BQ_END That's a huge leap, let's take a step back. I see two questions that should be answered first. 1) do we really want pattern in case of assignment/declaration to support assignment conversions ? 2) do we want patterns used by the switch or instanceof to follow the exact same rules as patterns used in assignment/declaration ? BQ_END I agree we should take a step back, but let's take a step farther -- because I want to make an even bigger leap that you think :) Stepping way far back .... in the beginning ... Java had reference types with subtyping, and eight primitive types. Which raises an immediate question: what types can be assigned to what? Java chose a sensible guideline; assignment should be allowed if the value set on the left is "bigger" than that on the right. This gives us String => Object, int => long, int => double, etc. (At this point, note that we've gone beyond strict value set inclusion; an int is *not* a floating point number, but we chose (reasonably) to do the conversion because we can *embed* the ints in the value set of double. Java was already appealing to the notion of embedding-projection pair even then, in assignment conversions; assignment from A to B is OK if we have an embedding of A into B.) On the other hand, Java won't let you assign long => int, because it might be a lossy conversion. To opt into the loss, you have to cast, which acknowledges that the conversion may be information-losing. Except! If you can prove the conversion isn't information losing (because the thing on the right is a compile-time constant), then its OK, because we know its safe. JLS Ch5 had its share of ad-hoc-seeming complexity, but mostly stayed in its corner until you called it, and the rules all seemed justifiable. Then we added autoboxing. And boxing is not problematic; int embeds into Integer. So the conversion from int => Integer is fine. (It added more complexity to overload selection, brought in strict and loose conversion contexts, and we're still paying when methods like remove(int) merge with remove(T), but OK.) But the other direction is problematic; there is one value of Integer that doesn't correspond to any value of int, which is our favorite value, null. The decision made at the time was to allow the conversion from Integer => int, and throw on null. This was again a justifiable choice, and comes from the fact that the mapping from Integer to int is a _projection_, not an embedding. It was decided (reasonably, but we could have gone the other way too) that null was a "silly" enough value to justify not requiring a cast, and throwing if the silly value comes up. We could have required a cast from Integer to int, as we do from long to int, and I can imagine the discussion about why that was not chosen. Having set the stage, one can see all the concepts in pattern matching dancing on it, just with different names. Whether we can assign T to U with or without a cast, is something we needed a static rule for. So we took the set of type pairs (T, U) for which the pattern `T t` is strictly total on U, and said "these are the conversions allowed in assignment context" (with a special rule for when the target is an integer constant.) When we got to autoboxing, we made a subjective call that `int x` should be "total enough" on `Integer` that we're willing to throw in the one place it's not. That's exactly the concept of "P is exhaustive, but not total, on T" (i.e., there is a non-empty remainder.) All of this has happened before. All of this will happen again. So the bigger leap I've got in mind is: what would James et al have done, had they had pattern matching from day one? I believe that: - T t = u would be allowed if `T t` is exhaustive on the static type of u; - If there is remainder, assignment can throw (preserving the invariant that if the assignment completes normally, something was assigned). So it's not that I want to align assignment with pattern matching because we've got a syntactic construct on the whiteboard that operates by pattern matching but happens to looks like assignment; it's because assignment *is* a constrained case of pattern matching. We've found the missing primitive, and I want to put it under the edifice. If we define pattern matching correctly, we could rewrite JLS 5.2 entirely in terms of pattern matching (whether we want to actually rewrite it or not, that's a separate story.) The great thing about pattern matching as a generalization of assignment is that it takes pressure off the one-size-fits-all ruleset. BQ_END Pattern matching is not a generalization of assignment, it's a generalization of the declaration + assignment, not assignment only. Apart if you are suggesting that we should extend pattern matching to be able to bound an existing variable ? BQ_BEGIN You can write: int x = anInteger but it might throw NPE. In many cases, users are fine with that. But by interpreting it as a pattern, when we get into more flexible constructs, we don't *have* to throw eagerly. If the user said: if (anInteger instanceof int x) { ... } then we match the pattern on everything but null, and don't match on null, and since instanceof is a conditional construct, no one needs to throw at all. And if the user said: switch (anInteger) { case int x: ... // I can have more cases } the `int x` case is taken for all values other than null, and the user has a choice to put more patterns that will catch up the remainder and act on them, or not; and if the user chooses "not", the remainder is implicitly rejected by switches handling of "exhaustive with remainder", but the timing of such is moved later, after the user has had as many bites at the apple as desired before we throw. The rules about assignment are the way they are because there's no statically trackable side-channel for "was it a good match". Now there is; let's use it. So, enough philosophy; on to the specific objections. BQ_BEGIN For 1, given that we are using pattern to do destructured assignment, we may want to simplify the assignment rules to keep things simple avoid users shooting themselves in the foot with implicit unboxing. With an example, record Box(T value) {} Box box = ... Box<>(int result) = box; // assignment of result may throw a NPE BQ_END I assume you mean "let Box... = box". Assuming so, let's analyze the above. BQ_END 'let' as keyword is not necessary here and cause more harm than good for everybody that switch regularly between JS and Java, but let us not be sidetracked by that (pun intended). BQ_BEGIN The pattern Box(P) is exhaustive on Box if P is exhaustive on Integer. If we say that `int result` is exhaustive on Integer (which I'm proposing), then the remainder of `Box(int result)` will be { null, Box(null) }. The pattern won't match the remainder (but *matching* does not throw), but the let/bind construct says "OK, if there was remainder, throw" (unless there's an else clause, yada yada.) So yes, the above would throw (I don't think it should throw NPE, but we're going to discuss that in a separate thread) not because of the pattern -- pattern matching *never* throws -- but because the let construct wants exhaustiveness, and accepts that some patterns have remainder, and makes up the difference by completing abruptly. It's just like: Box> bbs = new Box(null); let Box(Box(String s)) = bbs; Here, Box(null) is in the remainder of Box(Box(String s)) on Box>, so the match fails, and the construct throws. Unboxing is not really any different, and I wouldn't want to treat them differently (and I worry that yet again, there's some "anti-null bias" going on.) In both cases, we have a nested pattern that is exhaustive but has non-empty remainder. BQ_BEGIN I don't think we have to support that implicit unboxing given that we have a way to ask for an unboxing explicitly (once java.lang.Integer have a de-constructor) Box<>(Integer(int result)) = box; BQ_END This is just moving the remainder somewhere else; Box(null) is in the remainder of Box(Integer(BLAH)), since deconstructing the Integer requires invoking a deconstructor whose receiver would be null. I think what you're reaching for here is "the user should have to explicitly indicate that the conversion might not succeed", which would be analogous to "the user should have to cast Integer to int". But we've already made that decision; we don't require such a cast. BQ_BEGIN I think we should not jump with the shark too soon here and ask ourselves if we really want assignment conversions in case of destructured assignment. BQ_END See above; I think this is the wrong question. It is not a matter of "do we want assignment conversions in destructuring", it is "do we want to be able to *derive* the assignment conversions from pattern matching." BQ_END Pattern matching is a restrictive form of assignment, given that it always creates a fresh variable, so your analogy breaks here. Worst, there are other existing analogies, that are also perfectly valid - 'var' is also a restrictive form of assignment that always creates a fresh variable like pattern matching, and 'var' as different rules as assignment. - there is also another construct that creates fresh variables, lambdas. And lambdas does not allow assignment conversions. In both cases, you can run with that analogy as eloquently as you are doing with the "pattern matching is assignment" analogy. I'm not against allowing assignment conversions on pattern in the context of an assignment but in a switch or in an instanceof (when the context does not require totality) it means introducing a new semantics i really dislike (cf the sister message). R?mi -------------- next part -------------- An HTML attachment was scrubbed... URL: From forax at univ-mlv.fr Mon Feb 28 12:50:38 2022 From: forax at univ-mlv.fr (forax at univ-mlv.fr) Date: Mon, 28 Feb 2022 13:50:38 +0100 (CET) Subject: [External] : Re: Primitive type patterns In-Reply-To: <697fca40-9579-90df-1f9f-57993ad1bbd2@oracle.com> References: <1085786642.8814171.1645876949816.JavaMail.zimbra@u-pem.fr> <697fca40-9579-90df-1f9f-57993ad1bbd2@oracle.com> Message-ID: <160809176.9191102.1646052638127.JavaMail.zimbra@u-pem.fr> From: "Brian Goetz" To: "Remi Forax" Cc: "amber-spec-experts" Sent: Saturday, February 26, 2022 5:49:08 PM Subject: Re: [External] : Re: Primitive type patterns BQ_BEGIN BQ_BEGIN BQ_BEGIN #### Relationship with assignment context BQ_END That's a huge leap, let's take a step back. I see two questions that should be answered first. 1) do we really want pattern in case of assignment/declaration to support assignment conversions ? 2) do we want patterns used by the switch or instanceof to follow the exact same rules as patterns used in assignment/declaration ? BQ_END BQ_END [...] BQ_BEGIN BQ_BEGIN 2) we already know that depending on the context (inside a switch, inside a instanceof, inside an assignment) the rules for pattern are not exactly the same. BQ_END OMG Remi, would you please stop repeating this incorrect claim. The rules for pattern matching are exactly the same across contexts; the differences are that the contexts get to choose when to try to match, and what to do if nothing matches. BQ_END if (o instanceof Circle(Point(int x, int y), int radius)) means if (o instanceof Circle circle) { Point center = circle.center(); if (center == null) { goto end } int x = center.x(); int y = center.y(); int radius = circle.radius(); end: } while Circle(Point(int x, int y), int radius) = c; means Point center = circle.center(); if (center == null) { throw new NPE() } int x = center.x(); int y = center.y(); int radius = circle.radius(); end: In the context of switch or instanceof, a pattern does not have to be total, while in case of a assignment the pattern has to be total, so depending on the context, the semantics is different. R?mi -------------- next part -------------- An HTML attachment was scrubbed... URL: From brian.goetz at oracle.com Mon Feb 28 14:34:02 2022 From: brian.goetz at oracle.com (Brian Goetz) Date: Mon, 28 Feb 2022 09:34:02 -0500 Subject: [External] : Re: Primitive type patterns In-Reply-To: <1874101248.9190368.1646052603982.JavaMail.zimbra@u-pem.fr> References: <1085786642.8814171.1645876949816.JavaMail.zimbra@u-pem.fr> <697fca40-9579-90df-1f9f-57993ad1bbd2@oracle.com> <1874101248.9190368.1646052603982.JavaMail.zimbra@u-pem.fr> Message-ID: > > Nope, > i'm saying that inside a pattern if we let the unboxing to be possible > with the semantics that if the value is null, instead of throwing a > NPE if it does not match, we are introducing the equivalent of the > null-safe operator of Groovy (the elvis operator),? something we > should not do (and that was rejected in the past, i think as part of > Coin or Coin 2). Too silly to respond to.? Let's stay serious, please. > Maybe it means that we should allow unboxing with the semantics that > it throw a NPE i.e the exact semantics of the assignment conversion > instead of disallowing unboxing as i proposed, I don't know, but I > really dislike the semantics you are proposing here. It seems more likely that you just *don't understand* what is being proposed here (and the fact that you keep repeating the incorrect claim that pattern matching somehow means different things in different constructs underscores that.) Let me try explaining the latter one more time, because you really do seem to have a deep misundestanding of the semantics being proposed, and until we can clear that up, I'm not sure there's much point in arguing about the finer points. > ? if (o instanceof Circle(Point(int x, int y), int radius)) > means ... doesn't match ... > while > ? Circle(Point(int x, int y), int radius) = c; > means ? ... throws ... > In the context of switch or instanceof, a pattern does not have to be > total, while in case of a assignment the pattern has to be total, > so depending on the context, the semantics is different. No, this is still incorrect.? The semantics of the *pattern match* does not change.? Ever.? Period.? The pattern Circle(Point(int x, int y), int radius) does not match `new Circle(null, 3)`, ever. Ever.? EVER.? Got it?? It DOES NOT MATCH.? Pattern matching is 1000% consistent on this point.? What you've observed is a statement about instanceof vs switch vs let, not about pattern matching at all. What is different is that we have different constructs (instanceof, switch, let, try--catch, etc) which use pattern matching.? One (instanceof) evaluates to false when the operand is null (it doesn't even try testing the pattern), and doesn't require the pattern to be (statically) exhaustive, because it is a boolean expression.? Let, on the other hand, requires the pattern to be exhaustive, but tests the pattern anyway, and if it does not match, throws (just like switch throws on null when there is no null case.) The difference here is entirely "when does the construct decide to try to match" (e.g., switch can throw NPE before trying to match any cases if it likes) and "what does the construct do when it runs out of patterns to try" (instanceof says false, switch and let throw, try...catch would eventually allow the exception to propagate. -------------- next part -------------- An HTML attachment was scrubbed... URL: From brian.goetz at oracle.com Mon Feb 28 16:50:25 2022 From: brian.goetz at oracle.com (Brian Goetz) Date: Mon, 28 Feb 2022 11:50:25 -0500 Subject: Primitive type patterns In-Reply-To: References: Message-ID: <8d47550c-b9c7-8eea-bb71-043b282984da@oracle.com> Let me put the central question here in the spotlight. > #### Boxing and unboxing > > Suppose our match target is a box type, such as: > > ??? record IntegerBox(Integer i) { } > > Clearly we can match it with: > > ??? case IntegerBox(Integer i): We could stop here, and say the pattern `int i` is not applicable to `Integer`, which means that `IntegerBox(int i)` is not applicable to IntegerBox.? This is a legitimate choice (though, I think it would be a bad one, one that we would surely revisit sooner rather than later.) Users might well not understand why they could not say ??? case IntegerBox(int i) because (a) you can unbox in other contexts and (b) this has a very clear meaning, and one analogous to `case ObjectBox(String s)` -- if the box contents fits in a String, then match, otherwise don't. They might get even more balky if you could say ??? int x = anInteger but not ??? let int x = anInteger And, if we went with both Remi's preferences -- the minimalist matching proposal and dropping "let" in the let/bind syntax (which we will NOT discuss further here) then the following would be ambiguous: ??? int x = anInteger? // is this an assignment, in which case we unbox, or a match, in which case we don't? and we would have to "arbitrarily" pick the legacy interpretation. But all of this "balking" is symptom, not disease. The reality is that pattern matching is more primitive than unboxing conversions, and if we lean into that, things get simpler.? An unboxing conversion may seem like one thing, but is actually two: try to match against a partial pattern, and if there is no match, fail.? In other words, unboxing is: ??? int unbox(Integer n) -> ??????? switch(n) { ??????????? case int i -> i; ??????????? case null -> throw new NPE(); ???? ?? } The unboxing we have jams together the pattern match and the throw-on-fail, because it has no choice; unboxing wants to be total, and there's no place to specify what to try if the pattern match fails.? But pattern matching is inherently conditional, allowing us to build different constructs on it which handle failures differently. So *of course* there's an obvious definition of how `int x` matches against Integer, and its not a question of whether we "define" it that way, its a question of whether we expose the obvious meaning, or suppress it.? I think the arguments in favor of suppression are pretty weak. There's a similar argument when it comes to narrowing from (say) long to int.? There's a very natural interpretation of matching `int x` to a long: does the long value fit in an int.? In assignment context, we do the best we currently can -- we allow the narrowing only if we can statically prove it will match, which mean if the match target is a constant.? But again, conversions in assignment context are not the primitive.? If we take the obvious definition of matching `int x` against long, then the current rules fall out naturally, and we can ask sensible questions like ??? if (aLong instanceof byte) { ... } ??? else if (aLong instanceof short) { ... } by *asking the primitive directly*, rather than appealing to some proxy (like manually unrolling the range check.) -------------- next part -------------- An HTML attachment was scrubbed... URL: From forax at univ-mlv.fr Mon Feb 28 17:43:45 2022 From: forax at univ-mlv.fr (Remi Forax) Date: Mon, 28 Feb 2022 18:43:45 +0100 (CET) Subject: Primitive type patterns In-Reply-To: <8d47550c-b9c7-8eea-bb71-043b282984da@oracle.com> References: <8d47550c-b9c7-8eea-bb71-043b282984da@oracle.com> Message-ID: <445101940.9418309.1646070225203.JavaMail.zimbra@u-pem.fr> > From: "Brian Goetz" > To: "amber-spec-experts" > Sent: Monday, February 28, 2022 5:50:25 PM > Subject: Re: Primitive type patterns > Let me put the central question here in the spotlight. >> #### Boxing and unboxing >> Suppose our match target is a box type, such as: >> record IntegerBox(Integer i) { } >> Clearly we can match it with: >> case IntegerBox(Integer i): > We could stop here, and say the pattern `int i` is not applicable to `Integer`, > which means that `IntegerBox(int i)` is not applicable to IntegerBox. This is a > legitimate choice (though, I think it would be a bad one, one that we would > surely revisit sooner rather than later.) > Users might well not understand why they could not say > case IntegerBox(int i) > because (a) you can unbox in other contexts and (b) this has a very clear > meaning, and one analogous to `case ObjectBox(String s)` -- if the box contents > fits in a String, then match, otherwise don't. They might get even more balky > if you could say > int x = anInteger > but not > let int x = anInteger > And, if we went with both Remi's preferences -- the minimalist matching proposal > and dropping "let" in the let/bind syntax (which we will NOT discuss further > here) then the following would be ambiguous: > int x = anInteger // is this an assignment, in which case we unbox, or a match, > in which case we don't? > and we would have to "arbitrarily" pick the legacy interpretation. I said the opposite, if you use "let" you do not have to support assignment conversions. > But all of this "balking" is symptom, not disease. > The reality is that pattern matching is more primitive than unboxing > conversions, and if we lean into that, things get simpler. An unboxing > conversion may seem like one thing, but is actually two: try to match against a > partial pattern, and if there is no match, fail. In other words, unboxing is: > int unbox(Integer n) -> > switch(n) { > case int i -> i; > case null -> throw new NPE(); > } > The unboxing we have jams together the pattern match and the throw-on-fail, > because it has no choice; unboxing wants to be total, and there's no place to > specify what to try if the pattern match fails. But pattern matching is > inherently conditional, allowing us to build different constructs on it which > handle failures differently. > So *of course* there's an obvious definition of how `int x` matches against > Integer, and its not a question of whether we "define" it that way, its a > question of whether we expose the obvious meaning, or suppress it. I think the > arguments in favor of suppression are pretty weak. The strong argument is that instanceof/switch case is about subtyping relationship while assignment is about assignment conversions, trying to blur the lines between the two has already been tried in the past and it results in pain (see below). But i suppose every generations need to re-discover it. > There's a similar argument when it comes to narrowing from (say) long to int. > There's a very natural interpretation of matching `int x` to a long: does the > long value fit in an int. In assignment context, we do the best we currently > can -- we allow the narrowing only if we can statically prove it will match, > which mean if the match target is a constant. But again, conversions in > assignment context are not the primitive. If we take the obvious definition of > matching `int x` against long, then the current rules fall out naturally, and > we can ask sensible questions like > if (aLong instanceof byte) { ... } > else if (aLong instanceof short) { ... } > by *asking the primitive directly*, rather than appealing to some proxy (like > manually unrolling the range check.) What about ? Object o =... if (o instanceof byte) { ... } Does it means that o can be a Long ? R?mi -------------- next part -------------- An HTML attachment was scrubbed... URL: From forax at univ-mlv.fr Mon Feb 28 18:07:19 2022 From: forax at univ-mlv.fr (forax at univ-mlv.fr) Date: Mon, 28 Feb 2022 19:07:19 +0100 (CET) Subject: [External] : Re: Primitive type patterns In-Reply-To: References: <1085786642.8814171.1645876949816.JavaMail.zimbra@u-pem.fr> <697fca40-9579-90df-1f9f-57993ad1bbd2@oracle.com> <1874101248.9190368.1646052603982.JavaMail.zimbra@u-pem.fr> Message-ID: <1406300495.9422028.1646071639124.JavaMail.zimbra@u-pem.fr> > From: "Brian Goetz" > To: "Remi Forax" > Cc: "amber-spec-experts" > Sent: Monday, February 28, 2022 3:34:02 PM > Subject: Re: [External] : Re: Primitive type patterns >> Nope, >> i'm saying that inside a pattern if we let the unboxing to be possible with the >> semantics that if the value is null, instead of throwing a NPE if it does not >> match, we are introducing the equivalent of the null-safe operator of Groovy >> (the elvis operator), something we should not do (and that was rejected in the >> past, i think as part of Coin or Coin 2). > Too silly to respond to. Let's stay serious, please. >> Maybe it means that we should allow unboxing with the semantics that it throw a >> NPE i.e the exact semantics of the assignment conversion instead of disallowing >> unboxing as i proposed, I don't know, but I really dislike the semantics you >> are proposing here. > It seems more likely that you just *don't understand* what is being proposed > here (and the fact that you keep repeating the incorrect claim that pattern > matching somehow means different things in different constructs underscores > that.) [....] It's your view of the world you think that you can subsume all pattern usages by saying that they obey to the assignment conversions and you think you can decompose the unboxing as nullcheck + do not match. I believe that you are wrong on both counts, the former results in an over-generalization that will transfer part of the work the compiler does at compile time at runtime and the later means you are choosing in place of the user if it means Integer(int value) or Integer instead of asking. Given that the first point subsumes the second one, i think we should focus on it, first. So with record Atom(Object o) { ... } Atom box = ...; if (o instanceof Atom(byte b)) { } can box = new Atom(3); matches the pattern at runtime ? R?mi -------------- next part -------------- An HTML attachment was scrubbed... URL: From brian.goetz at oracle.com Mon Feb 28 18:08:07 2022 From: brian.goetz at oracle.com (Brian Goetz) Date: Mon, 28 Feb 2022 13:08:07 -0500 Subject: [External] : Re: Primitive type patterns In-Reply-To: <445101940.9418309.1646070225203.JavaMail.zimbra@u-pem.fr> References: <8d47550c-b9c7-8eea-bb71-043b282984da@oracle.com> <445101940.9418309.1646070225203.JavaMail.zimbra@u-pem.fr> Message-ID: <0e6b0a6c-5148-2dd6-5195-d4ea5e5ea89d@oracle.com> > > So *of course* there's an obvious definition of how `int x` > matches against Integer, and its not a question of whether we > "define" it that way, its a question of whether we expose the > obvious meaning, or suppress it.? I think the arguments in favor > of suppression are pretty weak. > > > The strong argument is that instanceof/switch case is about subtyping > relationship while assignment is about assignment conversions, trying > to blur the lines between the two has already been tried in the past > and it results in pain (see below). This is pretty much assuming your conclusion, and then stating it as justification :) I get it; you would prefer that pattern matching be *only* about subtyping.? I understand why you prefer that.? But I think this is mostly a "retreat to the comfort zone" thing. > > What about ? > > Object o =... > if (o instanceof byte) { ... } > > Does it means that o can be a Long ? > This is a good question.? (But I know it's also a trap.)? We first have to ask about (static) applicability: is the pattern `byte b` applicable to Object?? If not, we'll get a compilation error. My earlier message said: ?- A primitive type pattern `P p` should be applicable to a reference target T if T unboxes to P, or T unboxes to a primitive type that can be widened to P [ or if T unboxes to a primitive type that can be narrowed to P. ] Does Object unbox to byte?? No. Does Object unbox to a primitive type that can be widened to byte?? No. [brackets] Does Object unbox to a primitive type than can be narrowed to byte?? No. How does this square with assignments?? I cannot assign ??? byte b = anObject |? incompatible types: java.lang.Object cannot be converted to byte If I try this with casting: ?? Object o = 0L ?? byte b = (byte) o I get a CCE, because the cast will only convert from Byte. Now, what if instead of Object, we start with Long? ??? Long l = 0L ??? if (l instanceof byte b) { ... } First, applicability: does Long unbox to a primitive type that can be narrowed to byte?? Yes!? Long unboxes to long, and long can be narrowed to byte. Then: matching: if the RHS is not null, we unbox, and do a range check.? (The rules in my previous mail probably didn't get this case perfectly right), but 0L will match, and 0xffff will not -- as we would expect. We could consider pushing this farther, if we liked, but there's a risk of pushing it too far, and I think we're already on the cusp of diminishing returns.? We could consider `case byte b` against Object to match a Byte.? We discussed this, in fact, a few years ago when we talked about "what does `case 0` mean" when we were considering constant patterns.? (And we concluded in that discussion that the step where we basically treat Long as narrowable to Integer was taking it way too far.)? So I think the rules I've specified capture (a) the right set of tradeoffs of how loose we want to be with regard to boxing/unboxing combined with narrowing/widening, subject to (b) the existing decisions we've made about assignments. -------------- next part -------------- An HTML attachment was scrubbed... URL: From forax at univ-mlv.fr Mon Feb 28 18:34:49 2022 From: forax at univ-mlv.fr (forax at univ-mlv.fr) Date: Mon, 28 Feb 2022 19:34:49 +0100 (CET) Subject: [External] : Re: Primitive type patterns In-Reply-To: <0e6b0a6c-5148-2dd6-5195-d4ea5e5ea89d@oracle.com> References: <8d47550c-b9c7-8eea-bb71-043b282984da@oracle.com> <445101940.9418309.1646070225203.JavaMail.zimbra@u-pem.fr> <0e6b0a6c-5148-2dd6-5195-d4ea5e5ea89d@oracle.com> Message-ID: <1252930995.9426030.1646073289788.JavaMail.zimbra@u-pem.fr> > From: "Brian Goetz" > To: "Remi Forax" > Cc: "amber-spec-experts" > Sent: Monday, February 28, 2022 7:08:07 PM > Subject: Re: [External] : Re: Primitive type patterns >>> So *of course* there's an obvious definition of how `int x` matches against >>> Integer, and its not a question of whether we "define" it that way, its a >>> question of whether we expose the obvious meaning, or suppress it. I think the >>> arguments in favor of suppression are pretty weak. >> The strong argument is that instanceof/switch case is about subtyping >> relationship while assignment is about assignment conversions, trying to blur >> the lines between the two has already been tried in the past and it results in >> pain (see below). > This is pretty much assuming your conclusion, and then stating it as > justification :) > I get it; you would prefer that pattern matching be *only* about subtyping. I > understand why you prefer that. But I think this is mostly a "retreat to the > comfort zone" thing. >> What about ? >> Object o =... >> if (o instanceof byte) { ... } >> Does it means that o can be a Long ? > This is a good question. (But I know it's also a trap.) We first have to ask > about (static) applicability: is the pattern `byte b` applicable to Object? If > not, we'll get a compilation error. > My earlier message said: > - A primitive type pattern `P p` should be applicable to a reference target T if > T unboxes to P, or T unboxes to a primitive type that can be widened to P [ or > if T unboxes to a primitive type that can be narrowed to P. ] > Does Object unbox to byte? No. > Does Object unbox to a primitive type that can be widened to byte? No. > [brackets] Does Object unbox to a primitive type than can be narrowed to byte? > No. > How does this square with assignments? I cannot assign > byte b = anObject > | incompatible types: java.lang.Object cannot be converted to byte > If I try this with casting: > Object o = 0L > byte b = (byte) o > I get a CCE, because the cast will only convert from Byte. > Now, what if instead of Object, we start with Long? > Long l = 0L > if (l instanceof byte b) { ... } > First, applicability: does Long unbox to a primitive type that can be narrowed > to byte? Yes! Long unboxes to long, and long can be narrowed to byte. > Then: matching: if the RHS is not null, we unbox, and do a range check. (The > rules in my previous mail probably didn't get this case perfectly right), but > 0L will match, and 0xffff will not -- as we would expect. This is totally alien to me, when you have x instanceof Foo (note: this is not the pattern version) with X the type of x, then if x is declared with a super type of X it works the exact same way, i.e i don't have to care to much about the type on what i'm doing an instanceof / switching over it. The rule you propose breaks that, i've to take a very close look to the type of 'x'. As i said earlier, it's too smart for me. I see no problem to have such semantics specified by a pattern method, but i don't think we should make the type pattern too clever. R?mi -------------- next part -------------- An HTML attachment was scrubbed... URL: From brian.goetz at oracle.com Mon Feb 28 18:53:07 2022 From: brian.goetz at oracle.com (Brian Goetz) Date: Mon, 28 Feb 2022 13:53:07 -0500 Subject: [External] : Re: Primitive type patterns In-Reply-To: <1252930995.9426030.1646073289788.JavaMail.zimbra@u-pem.fr> References: <8d47550c-b9c7-8eea-bb71-043b282984da@oracle.com> <445101940.9418309.1646070225203.JavaMail.zimbra@u-pem.fr> <0e6b0a6c-5148-2dd6-5195-d4ea5e5ea89d@oracle.com> <1252930995.9426030.1646073289788.JavaMail.zimbra@u-pem.fr> Message-ID: <82a8dd98-b8b2-e54e-4c2c-8a1c675614f2@oracle.com> > Now, what if instead of Object, we start with Long? > > ??? Long l = 0L > ??? if (l instanceof byte b) { ... } > > First, applicability: does Long unbox to a primitive type that can > be narrowed to byte?? Yes!? Long unboxes to long, and long can be > narrowed to byte. > > Then: matching: if the RHS is not null, we unbox, and do a range > check.? (The rules in my previous mail probably didn't get this > case perfectly right), but 0L will match, and 0xffff will not -- > as we would expect. > > > This is totally alien to me, when you have x instanceof Foo (note: > this is not the pattern version) with X the type of x, then if x is > declared with a super type of X it works the exact same way, i.e i > don't have to care to much about the type on what i'm doing an > instanceof / switching over it. Yes, I understand your discomfort.? And I will admit, I don't love this particular corner-of-a-corner either.? (But let's be clear: it is a corner.? If you're seeking to throw out the whole scheme on the basis that corners exist, you'll find the judge to be unsympathetic.) So why have I proposed it this way?? Because, unfortunately, of this existing line in JLS 5.2 (which I never liked): > an unboxing conversion followed by a widening primitive conversion This is what lets you say: ??? long l = anInteger And, I never liked this rule, but we're stuck with it.? The inverse, from which we would derive this rule, is that ??? anInteger instanceof long l should be applicable, and in fact always match when the LHS is non-null.? I would prefer to not allow this assignment conversion, and similarly not allow both unboxing and widening in one go in pattern matching, but I didn't get to write JLS 5.2. What's new here is going in the *other* direction: ??? anInteger instanceof short s and I think what is making you uncomfortable is that you are processing two generalizations at once, and it's pushing your "OMG different! scary!" buttons: ?- that we're defining primitive type patterns in a way such that we can derive the existing assignment conversions; ?- that primitive type patterns can have dynamic checks that primitive assignments cannot, so we're including the value-range check. Each individually is not quite as scary, but I can understand why the two together would seem scary.? (And, as I mentioned, I don't like the unbox-and-widen conversions either, but I didn't invent those.) -------------- next part -------------- An HTML attachment was scrubbed... URL: From forax at univ-mlv.fr Mon Feb 28 20:35:32 2022 From: forax at univ-mlv.fr (forax at univ-mlv.fr) Date: Mon, 28 Feb 2022 21:35:32 +0100 (CET) Subject: [External] : Re: Primitive type patterns In-Reply-To: <82a8dd98-b8b2-e54e-4c2c-8a1c675614f2@oracle.com> References: <8d47550c-b9c7-8eea-bb71-043b282984da@oracle.com> <445101940.9418309.1646070225203.JavaMail.zimbra@u-pem.fr> <0e6b0a6c-5148-2dd6-5195-d4ea5e5ea89d@oracle.com> <1252930995.9426030.1646073289788.JavaMail.zimbra@u-pem.fr> <82a8dd98-b8b2-e54e-4c2c-8a1c675614f2@oracle.com> Message-ID: <1221659857.9463108.1646080532106.JavaMail.zimbra@u-pem.fr> > From: "Brian Goetz" > To: "Remi Forax" > Cc: "amber-spec-experts" > Sent: Monday, February 28, 2022 7:53:07 PM > Subject: Re: [External] : Re: Primitive type patterns >>> Now, what if instead of Object, we start with Long? >>> Long l = 0L >>> if (l instanceof byte b) { ... } >>> First, applicability: does Long unbox to a primitive type that can be narrowed >>> to byte? Yes! Long unboxes to long, and long can be narrowed to byte. >>> Then: matching: if the RHS is not null, we unbox, and do a range check. (The >>> rules in my previous mail probably didn't get this case perfectly right), but >>> 0L will match, and 0xffff will not -- as we would expect. >> This is totally alien to me, when you have x instanceof Foo (note: this is not >> the pattern version) with X the type of x, then if x is declared with a super >> type of X it works the exact same way, i.e i don't have to care to much about >> the type on what i'm doing an instanceof / switching over it. > Yes, I understand your discomfort. And I will admit, I don't love this > particular corner-of-a-corner either. (But let's be clear: it is a corner. If > you're seeking to throw out the whole scheme on the basis that corners exist, > you'll find the judge to be unsympathetic.) > So why have I proposed it this way? Because, unfortunately, of this existing > line in JLS 5.2 (which I never liked): > > an unboxing conversion followed by a widening primitive conversion > This is what lets you say: > long l = anInteger > And, I never liked this rule, but we're stuck with it. The inverse, from which > we would derive this rule, is that > anInteger instanceof long l > should be applicable, and in fact always match when the LHS is non-null. I would > prefer to not allow this assignment conversion, and similarly not allow both > unboxing and widening in one go in pattern matching, but I didn't get to write > JLS 5.2. > What's new here is going in the *other* direction: > anInteger instanceof short s > and I think what is making you uncomfortable is that you are processing two > generalizations at once, and it's pushing your "OMG different! scary!" buttons: > - that we're defining primitive type patterns in a way such that we can derive > the existing assignment conversions; > - that primitive type patterns can have dynamic checks that primitive > assignments cannot, so we're including the value-range check. > Each individually is not quite as scary, but I can understand why the two > together would seem scary. (And, as I mentioned, I don't like the > unbox-and-widen conversions either, but I didn't invent those.) I think we should play on our strengths, destructuring instead of unboxing, pattern methods instead of primitive conversions + rangecheck. I prefer to have a few small well defined patterns (type pattern, type destructuring, pattern method) each with a simple semantics (so keep the type pattern to be about subtyping relationship only) and draw power from the ability to compose them instead of having patterns with a huge semantic baggage (type pattern with the assignment semantics) and the corner cases that come with it. We may still need assignment conversions when we mix pattern and assignment, but because we want to be "backward compatibility" with the simple assignment semantics. R?mi -------------- next part -------------- An HTML attachment was scrubbed... URL: From brian.goetz at oracle.com Mon Feb 28 21:00:08 2022 From: brian.goetz at oracle.com (Brian Goetz) Date: Mon, 28 Feb 2022 16:00:08 -0500 Subject: [External] : Re: Primitive type patterns In-Reply-To: <1221659857.9463108.1646080532106.JavaMail.zimbra@u-pem.fr> References: <8d47550c-b9c7-8eea-bb71-043b282984da@oracle.com> <445101940.9418309.1646070225203.JavaMail.zimbra@u-pem.fr> <0e6b0a6c-5148-2dd6-5195-d4ea5e5ea89d@oracle.com> <1252930995.9426030.1646073289788.JavaMail.zimbra@u-pem.fr> <82a8dd98-b8b2-e54e-4c2c-8a1c675614f2@oracle.com> <1221659857.9463108.1646080532106.JavaMail.zimbra@u-pem.fr> Message-ID: <51f07830-a1bf-b705-2c89-53e71ccb066e@oracle.com> This is a valid generalized preference (and surely no one is going to say "no, I prefer to play to our weaknesses.")? But at root, I think what you are saying is that you would prefer that pattern matching simply be a much smaller and less fundamental feature than what is being discussed here.? And again, while I think that's a valid preference, I think the basis of your preference is that it is "simpler", but I do not think it actually delivers the simplicity dividend you are hoping for, because there will be subtle mismatches that impede composition and refactoring (e.g., new "null gates" and "box gates".) In any case, it's clear that nearly everything about the way we've designed pattern matching is "not how you would have done it"; you don't like the totality, exhaustiveness, error handling, and conversion rules.? And that's fine, but I think we're spending too much effort on "I wish there were a different design center".? What I'd like to get feedback on is: ?- Is there anything *specifically wrong* with what is being proposed?? Did I extrapolate incorrectly from the rules for assignment conversion, for example? ?- How can we come to a better way of presenting the material, so that people don't fall into the same pothole regarding "pattern matching means different things in different places", for example? Would also like to hear from other people.... > I think we should play on our strengths, destructuring instead of > unboxing, pattern methods instead of primitive conversions + rangecheck. > I prefer to have a few small well defined patterns (type pattern, type > destructuring, pattern method) each with a simple semantics (so keep > the type pattern to be about subtyping relationship only) and draw > power from the ability to compose them instead of having patterns with > a huge semantic baggage (type pattern with the assignment semantics) > and the corner cases that come with it. > > We may still need assignment conversions when we mix pattern and > assignment, but because we want to be "backward compatibility" with > the simple assignment semantics. >