From amaembo at gmail.com Fri Dec 1 09:57:46 2023 From: amaembo at gmail.com (Tagir Valeev) Date: Fri, 1 Dec 2023 10:57:46 +0100 Subject: Java 17 preview language level use Message-ID: Hello! I guess there was already a discussion about whether it's a good idea to have preview features in Java versions that major vendors consider as LTS. I cannot find it right now, but I have some numbers, which might be interesting to experts. According to the stats that JetBrains gathers from the users (who agreed to send anonymous statistics), now at the end of 2023, about 1.1?1.2% of Java users still use Java 17-preview language level in at least one module of their projects. Among users who are at Java 17, the percentage is even higher, about 3.0?3.5%. This is more than 10x higher, compared to use of preview level of Java 18, 19, 20, or 21. As we know, it's a little bit too late to use it to evaluate and submit feedback, which is a primary purpose of the preview version. And we get quite angry feedback from the users because we stopped supporting Java 17-preview in our IDEs. So people really use preview features of LTS Java versions in production. Probably, skipping preview features in LTS versions would be better. At least, something to think about. With best regards, Tagir Valeev. -------------- next part -------------- An HTML attachment was scrubbed... URL: From archie.cobbs at gmail.com Fri Dec 1 15:24:26 2023 From: archie.cobbs at gmail.com (Archie Cobbs) Date: Fri, 1 Dec 2023 09:24:26 -0600 Subject: Java 17 preview language level use In-Reply-To: References: Message-ID: On Fri, Dec 1, 2023 at 3:58?AM Tagir Valeev wrote: > And we get quite angry feedback from the users because we stopped > supporting Java 17-preview in our IDEs. > Apologies if this is a dumb question... but what exactly do you mean by "we stopped supporting Java 17-preview in our IDEs" ? If I start up the IDE, configure Java 17 and want to have preview features enabled (i.e., turn on --enable-preview), are you saying I can't do that? -Archie -- Archie L. Cobbs -------------- next part -------------- An HTML attachment was scrubbed... URL: From amaembo at gmail.com Fri Dec 1 15:50:37 2023 From: amaembo at gmail.com (Tagir Valeev) Date: Fri, 1 Dec 2023 16:50:37 +0100 Subject: Java 17 preview language level use In-Reply-To: References: Message-ID: >If I start up the IDE, configure Java 17 and want to have preview features enabled (i.e., turn on --enable-preview), are you saying I can't do that? Yes. Similarly, if you install Java 21 and use `--enable-preview --release=17`. You can't do that. You have to install an older version of Java, or (in our case) an older version of IDE. With best regards, Tagir Valeev On Fri, Dec 1, 2023 at 4:24?PM Archie Cobbs wrote: > On Fri, Dec 1, 2023 at 3:58?AM Tagir Valeev wrote: > >> And we get quite angry feedback from the users because we stopped >> supporting Java 17-preview in our IDEs. >> > > Apologies if this is a dumb question... but what exactly do you mean by > "we stopped supporting Java 17-preview in our IDEs" ? > > If I start up the IDE, configure Java 17 and want to have preview features > enabled (i.e., turn on --enable-preview), are you saying I can't do that? > > -Archie > > -- > Archie L. Cobbs > -------------- next part -------------- An HTML attachment was scrubbed... URL: From alex.buckley at oracle.com Fri Dec 1 21:43:32 2023 From: alex.buckley at oracle.com (Alex Buckley) Date: Fri, 1 Dec 2023 13:43:32 -0800 Subject: Java 17 preview language level use In-Reply-To: References: Message-ID: Hi Tagir, Thanks for collecting and sharing this information about preview feature opt-in. On 12/1/2023 1:57 AM, Tagir Valeev wrote: > According to the stats that JetBrains gathers from the users (who agreed > to send anonymous statistics), now at the end of 2023, about 1.1?1.2% of > Java users still use Java 17-preview language level in at least one > module of their projects. To be clear, does "use Java 17-preview language level" mean: "They ticked a box to enable the use of 17's preview language features, but we don't know if they actually use pattern matching in switch in their code" (Pattern matching in switch was the only preview language feature in 17) or "They ticked the box and wrote at least one pattern-matching switch in their code" ? > Among users who are at Java 17, the percentage > is even higher, about 3.0?3.5%. This is more than 10x higher, compared > to use of preview level of Java 18, 19, 20, or 21. I am surprised that users on Java 17 have a 10x higher opt-in to preview features than users on Java 18-21. Among other things, Java 19 and 20 previewed virtual threads while Java 21 previews string templates and unnamed classes. Given the enormous interest in these features, I would have expected much higher preview opt-in _after_ 17 than in 17. (Again, the only preview feature in 17 was pattern matching in switch -- a nice feature to be sure, but frankly a sidebar for people migrating from 8/11, because so much good stuff appeared between 11 and 17.) > As we know, it's a little bit too late to use it to evaluate and submit > feedback, which is a primary purpose of the preview version. And we get > quite angry feedback from the users because we stopped supporting Java > 17-preview in our IDEs. So people really use preview features of LTS > Java versions in production. We have already come (somewhat reluctantly, at least in my case) to the conclusion that people use preview features of _any_ Java version in production. We know this because widely distributed libraries that ran on Java 19/20 either used or supported the use of preview APIs like FFM. > Probably, skipping preview features in LTS versions would be better. At > least, something to think about. I know what you mean, but in both principle and practice it's not possible. First, in principle: LTS is not an OpenJDK concept, so it's not something a feature owner in OpenJDK has any way to consider. Second, in practice: even if you said "Look, everyone knows Java 25 is the next release after 21 to get long-term updates from vendors, so please avoid having preview features in 25", then you're basically saying that preview features have to be squeezed into the 22 and 23 releases, so that they all have a high chance of finalizing in 24 or 25. This focus on special "preview-capable" releases goes against everything the time-driven "train" model stands for. It would mean that a feature owner who's ready to preview in 24 would be told "Sorry, you've missed the preview train, come back in a year's time to catch it in 26." This would frustrate feature owners (who thought they were free of schedule games) and confound users. There are also extreme practical difficulties if a feature previews in "non-LTS" releases but isn't quite ready to go final in time for your "LTS" release. Do you physically pull the feature out of the "LTS" codebase, or do you do the work to guarantee that preview features in the language, VM, and API are completely unusable in this release? The complexity is totally unwarranted. Alex From archie.cobbs at gmail.com Fri Dec 1 21:50:52 2023 From: archie.cobbs at gmail.com (Archie Cobbs) Date: Fri, 1 Dec 2023 15:50:52 -0600 Subject: Java 17 preview language level use In-Reply-To: References: Message-ID: On Fri, Dec 1, 2023 at 9:50?AM Tagir Valeev wrote: > >If I start up the IDE, configure Java 17 and want to have preview > features enabled (i.e., turn on --enable-preview), are you saying I can't > do that? > > Yes. Similarly, if you install Java 21 and use `--enable-preview > --release=17`. You can't do that. You have to install an older version of > Java, or (in our case) an older version of IDE. > I agree with Alex - In short, "preview feature" and "long term support" are orthogonal concepts, and therefore this seems like just a problem with an IDE limitation. -Archie -- Archie L. Cobbs -------------- next part -------------- An HTML attachment was scrubbed... URL: From orionllmain at gmail.com Sat Dec 2 04:09:59 2023 From: orionllmain at gmail.com (Zheka Kozlov) Date: Sat, 2 Dec 2023 10:09:59 +0600 Subject: Java 17 preview language level use In-Reply-To: References: Message-ID: On Sat, Dec 2, 2023 at 3:51?AM Archie Cobbs wrote: > I agree with Alex - In short, "preview feature" and "long term support" > are orthogonal concepts, and therefore this seems like just a problem with > an IDE limitation. > > -Archie > This is a problem with people using preview features in production. The IDE is fine. -------------- next part -------------- An HTML attachment was scrubbed... URL: From brian.goetz at oracle.com Tue Dec 12 21:23:09 2023 From: brian.goetz at oracle.com (Brian Goetz) Date: Tue, 12 Dec 2023 16:23:09 -0500 Subject: Effect cases in switch Message-ID: Based on some inspiration from OCaml, and given that the significant upgrades to switch so far position it to do a lot more than it could before, we've been exploring a further refinement of switch to incorporate failure handling as well. (I realize that this may elicit strong reactions from some, but please give it some careful thought before giving voice to those reactions.) # Uniform handling of failure in switch ## Summary Enhance the `switch` construct to support `case` labels that match exceptions thrown during evaluation of the selector expression, providing uniform handling of normal and exceptional results. ## Background The purpose of the `switch` construct is to choose a single course of action based on evaluating a single expression (the "selector").? The `switch` construct is not strictly needed in the language; everything that `switch` does can be done by `if-else`.? But the language includes `switch` because it embodies useful constraints which both streamline the code and enable more comprehensive error checking. The original version of `switch` was very limited: the selector expression was limited to a small number of primitive types, the `case` labels were limited to numeric literals, and the body of a switch was limited to operating by side-effects (statements only, no expressions.)? Because of these limitations, the use of `switch` was usually limited to low-level code such as parsers and state machines.? In Java 5 and 7, `switch` received minor upgrades to support primitive wrapper types, enums, and strings as selectors, but its role as "pick from one of these constants" did not change significantly. Recently, `switch` has gotten more significant upgrades, to the point where it can take on a much bigger role in day-to-day program logic.? Switch can now be used as an expression in addition to a statement, enabling greater composition and more streamlined code.? The selector expression can now be any type.? The `case` labels in a switch block can be rich patterns, not just constants, and have arbitrary predicates as guards.? We get much richer type checking for exhaustiveness when switching over selectors involving sealed types.? Taken together, this means much more program logic can be expressed concisely and reliably using `switch` than previously. ### Bringing nulls into `switch` Historically, the `switch` construct was null-hostile; if the selector evaluated to `null`, the `switch` immediately completed abruptly with `NullPointerException`.? This made a certain amount of sense when the only reference types that could be used in switch were primitive wrappers and enums, for which nulls were almost always indicative of an error, but as `switch` became more powerful, this was increasingly a mismatch for what we wanted to do with `switch`.? Developers were forced to work around this, but the workarounds had undesirable consequences (such as forcing the use of statement switches instead of expression switches.)? Previously, to handle null, one would have to separately evaluate the selector and compare it to `null` using `if`: ``` SomeType selector = computeSelector(); SomeOtherType result; if (selector == null) { ??? result = handleNull(); } else { ??? switch (selector) { ??????? case X: ??????????? result = handleX(); ??????????? break; ??????? case Y: ??????????? result = handleY(); ??????????? break; ??? } } ``` Not only is this more cumbersome and less concise, but it goes against the main job of `switch`, which is streamline "pick one path based on a selector expression" decisions.? Outcomes are not handled uniformly, they are not handled in one place, and the inability to express all of this as an expression limits composition with other language features. In Java 21, it became possible to treat `null` as just another possible value of the selector in a `case` clause (and even combine `null` handling with `default`), so that the above mess could reduce to ``` SomeOtherType result = switch (computeSelector()) { ??? case null -> handleNull(); ??? case X -> handleX(); ??? case Y -> handleY(); } ``` This is simpler to read, less error-prone, and interacts better with the rest of the language.? Treating nulls uniformly as just another value, as opposed to treating it as an out-of-band condition, made `switch` more useful and made Java code simpler and better.? (For compatibility, a `switch` that has no `case null` still throws `NullPointerException` when confronted with a null selector; we opt into the new behavior with `case null`.) ### Other switch tricks The accumulation of new abilities for `switch` means that it can be used in more situations than we might initially realize.? One such use is replacing the ternary conditional expression with boolean switch expressions; now that `switch` can support boolean selectors, we can replace ??? expr ? A : B with the switch expression ``` switch (expr) { ??? case true -> A; ??? case false -> B; } ``` This might not immediately seem preferable, since the ternary expression is more concise, but the `switch` is surely more clear.? And, if we nest ternaries in the arms of other ternaries (possibly deeply), this can quickly become unreadable, whereas the corresponding nested switch remains readable even if nested to several levels.? We don't expect people to go out and change all their ternaries to switches overnight, but we do expect that people will increasingly find uses where a boolean switch is preferable to a ternary.? (If the language had boolean switch expressions from day 1, we might well not have had ternary expressions at all.) Another less-obvious example is using guards to do the selection, within the bounds of the "pick one path" that `switch` is designed for.? For example, we can write the classic "FizzBuzz" exercise as: ``` String result = switch (getNumber()) { ??? case int i when i % 15 == 0 -> "FizzBuzz"; ??? case int i when i % 5 == 0 -> "Fizz"; ??? case int i when i % 3 == 0 -> "Buzz"; ??? case int i -> Integer.toString(i); } ``` A more controversial use of the new-and-improved switch is as a replacement for block expressions. Sometimes we want to use an expression (such as when passing a parameter to a method), but the value can only be constructed using statements: ``` String[] choices = new String[2]; choices[0] = f(0); choices[1] = f(1); m(choices); ``` While it is somewhat "off label", we can replace this with a switch expression: ``` m(switch (0) { ??? default -> { ??????? String[] choices = new String[2]; ??????? choices[0] = f(0); ??????? choices[1] = f(1); ??????? yield choices; ??? } }) ``` While these were not the primary use cases we had in mind when upgrading `switch`, it illustrates how the combination of improvements to `switch` have made it a sort of "swiss army knife". ## Handling failure uniformly Previously, null selector values were treated as out-of-band events, requiring that users handle null selectors in a non-uniform way.? The improvements to `switch` in Java 21 enable null to be handled uniformly as a selector value, as just another value. A similar source of out-of-band events in `switch` is exceptions; if evaluating the selector throws an exception, the switch immediately completes with that exception.? This is an entirely justifiable design choice, but it forces users to handle exceptions using a separate mechanism, often a cumbersome one, just as we did with null selectors: ``` Number parseNumber(String s) throws NumberFormatException() { ... } try { ??? switch (parseNumber(input)) { ??????? case Integer i -> handleInt(i); ??????? case Float f -> handleFloat(f); ??????? ... ??? } } catch (NumberFormatException e) { ??? ... handle exception ... } ``` This is already unfortunate, as switch is designed to handle "choose one path based on evaluating the selector", and "parse error" is one of the possible consequences of evaluating the selector.? It would be nice to be able to handle error cases uniformly with success cases, as we did with null. Worse, this code doesn't even mean what we want: the `catch` block catches not only exceptions thrown by evaluating the selector, but also by the body of the switch.? To say what we mean, we need the even more unfortunate ``` var answer = null; try { ??? answer = parseNumber(input); } catch (NumberFormatException e) { ??? ... handle exception ... } if (answer != null) { ??? switch (answer) { ??????? case Integer i -> handleInt(i); ??????? case Float f -> handleFloat(f); ??????? ... ??? } } ``` Just as it was an improvement to handle `null` uniformly as just another potential value of the selector expression, we can get a similar improvement by handling normal and exceptional completion uniformly as well. Normal and exceptional completion are mutually exclusive, and the handling of exceptions in `try-catch` already has a great deal in common with handling normal values in `switch` statements (a catch clause is effectively matching to a type pattern.) For activities with anticipated failure modes, handling successful completion via one mechanism and failed completion through another makes code harder to read and maintain. ## Proposal We can extend `switch` to handle exceptions more uniformly in a similar was as we extended it to handle nulls by introducing `throws` cases, which match when evaluating the selector expression completes abruptly with a compatible exception: ``` String allTheLines = switch (Files.readAllLines(path)) { ??? case List lines -> lines.stream().collect(Collectors.joining("\n")); ??? case throws IOException e -> ""; } ``` This captures the programmer's intent much more clearly, because the expected success case and the expected failure case are handled uniformly and in the same place, and their results can flow into the result of the switch expression. The grammar of `case` labels is extended to include a new form, `case throws`, which is followed by a type pattern: ??? case throws IOException e: Exception cases can be used in all forms of `switch`: expression and statement switches, switches that use traditional (colon) or single-consequence (arrow) case labels.? Exception cases can have guards like any other pattern case. Exception cases have the obvious dominance order with other exception cases (the same one used to validate order of `catch` clauses in `try-catch`), and do not participate in dominance ordering with non-exceptional cases.? It is a compile-time error if an exception case specifies an exception type that cannot be thrown by the selector expression, or a type that does not extend `Throwable`.? For clarity, exception cases should probably come after all other non-exceptional cases. When evaluating a `switch` statement or expression, the selector expression is evaluated.? If evaluation of the selector expression throws an exception, and one of the exception cases in the `switch` matches the exception, then control is transferred to the first exception case matching the exception. If no exception case matches the exception, then the switch completes abruptly with that same exception. This slightly adjusts the set of exceptions thrown by a `switch`; if an exception is thrown by the selector expression but not the body of the switch, and it is matched by an unguarded exception case, then the switch is not considered to throw that exception. ### Examples In some cases, we will want to totalize a partial computation by supplying a fallback value when there is an exception: ``` Function> safeParse = ??? s -> switch(Integer.parseInt(s)) { ??????????? case int i -> Optional.of(i); ??????????? case throws NumberFormatException _ -> Optional.empty(); ??? }; ``` In other cases, we may want to ignore exceptional values entirely: ``` stream.mapMulti((f, c) -> switch (readFileToString(url)) { ??????????????????? case String s -> c.accept(s); ??????????????????? case throws MalformedURLException _ -> { }; ??????????????? }); ``` In others, we may want to process the result of a method like `Future::get` more uniformly: ``` Future f = ... switch (f.get()) { ??? case String s -> process(s); ??? case throws ExecutionException(var underlying) -> throw underlying; ??? case throws TimeoutException e -> cancel(); } ``` ### Discussion We expect the reaction to this to be initially uncomfortable, because historically the `try` statement was the only way to control the handling of exceptions.? There is clearly still a role for `try` in its full generality, but just as `switch` profitably handles a constrained subset of the situations that could be handled with the more general `if-else` construct, there is similarly profit in allowing it to handle a constrained subset of the cases handled by the more general `try-catch` construct.? Specifically, the situation that `switch` is made for: evaluate an expression, and then choose one path based on the outcome of evaluating that expression, applies equally well to discriminating unsuccessful evaluations.? Clients will often want to handle exceptional as well as successful completion, and doing so uniformly within a single construct is likely to be clearer and less error-prone than spreading it over two constructs. Java APIs are full of methods that can either produce a result or throw an exception, such as `Future::get`.? Writing APIs in this way is natural for the API author, because they get to handle computation in a natural way; if they get to the point where they do not want to proceed, they can `throw` an exception, just as when they get to the point where the computation is done, they can `return` a value. Unfortunately, this convenience and uniformity for API authors puts an extra burden on API consumers; handling failures is more cumbersome than handling the successful case.? Allowing clients to `switch` over all the ways a computation could complete heals this rift. None of this is to say that `try-catch` is obsolete, any more than `switch` makes `if-else` obsolete.? When we have a large block of code that may fail at multiple points, handling all the exceptions from the block together is often more convenient than handling each exception at its generation point.? But when we scale `try-catch` down to a single expression, it can get awkward.? The effect is felt most severely with expression lambdas, which undergo a significant syntactic expansion if they want to handle their own exceptions. -------------- next part -------------- An HTML attachment was scrubbed... URL: From forax at univ-mlv.fr Tue Dec 12 22:41:23 2023 From: forax at univ-mlv.fr (Remi Forax) Date: Tue, 12 Dec 2023 23:41:23 +0100 (CET) Subject: Effect cases in switch In-Reply-To: References: Message-ID: <327307323.80228032.1702420883408.JavaMail.zimbra@univ-eiffel.fr> > From: "Brian Goetz" > To: "amber-spec-experts" > Sent: Tuesday, December 12, 2023 10:23:09 PM > Subject: Effect cases in switch > Based on some inspiration from OCaml, and given that the significant upgrades to > switch so far position it to do a lot more than it could before, we've been > exploring a further refinement of switch to incorporate failure handling as > well. > (I realize that this may elicit strong reactions from some, but please give it > some careful thought before giving voice to those reactions.) Hello, > # Uniform handling of failure in switch > ## Summary > Enhance the `switch` construct to support `case` labels that match exceptions > thrown during evaluation of the selector expression, providing uniform handling > of normal and exceptional results. > ## Background > The purpose of the `switch` construct is to choose a single course of action > based on evaluating a single expression (the "selector"). The `switch` > construct is not strictly needed in the language; everything that `switch` does > can be done by `if-else`. But the language includes `switch` because it > embodies useful constraints which both streamline the code and enable more > comprehensive error checking. > The original version of `switch` was very limited: the selector expression was > limited to a small number of primitive types, the `case` labels were limited to > numeric literals, and the body of a switch was limited to operating by > side-effects (statements only, no expressions.) Because of these limitations, > the use of `switch` was usually limited to low-level code such as parsers and > state machines. In Java 5 and 7, `switch` received minor upgrades to support > primitive wrapper types, enums, and strings as selectors, but its role as "pick > from one of these constants" did not change significantly. > Recently, `switch` has gotten more significant upgrades, to the point where it > can take on a much bigger role in day-to-day program logic. Switch can now be > used as an expression in addition to a statement, enabling greater composition > and more streamlined code. The selector expression can now be any type. The > `case` labels in a switch block can be rich patterns, not just constants, and > have arbitrary predicates as guards. We get much richer type checking for > exhaustiveness when switching over selectors involving sealed types. Taken > together, this means much more program logic can be expressed concisely and > reliably using `switch` than previously. > ### Bringing nulls into `switch` > Historically, the `switch` construct was null-hostile; if the selector evaluated > to `null`, the `switch` immediately completed abruptly with > `NullPointerException`. This made a certain amount of sense when the only > reference types that could be used in switch were primitive wrappers and enums, > for which nulls were almost always indicative of an error, but as `switch` > became more powerful, this was increasingly a mismatch for what we wanted to do > with `switch`. Developers were forced to work around this, but the workarounds > had undesirable consequences (such as forcing the use of statement switches > instead of expression switches.) Previously, to handle null, one would have to > separately evaluate the selector and compare it to `null` using `if`: > ``` > SomeType selector = computeSelector(); > SomeOtherType result; > if (selector == null) { > result = handleNull(); > } > else { > switch (selector) { > case X: > result = handleX(); > break; > case Y: > result = handleY(); > break; > } > } > ``` > Not only is this more cumbersome and less concise, but it goes against the main > job of `switch`, which is streamline "pick one path based on a selector > expression" decisions. Outcomes are not handled uniformly, they are not handled > in one place, and the inability to express all of this as an expression limits > composition with other language features. > In Java 21, it became possible to treat `null` as just another possible value of > the selector in a `case` clause (and even combine `null` handling with > `default`), so that the above mess could reduce to > ``` > SomeOtherType result = switch (computeSelector()) { > case null -> handleNull(); > case X -> handleX(); > case Y -> handleY(); > } > ``` > This is simpler to read, less error-prone, and interacts better with the rest of > the language. Treating nulls uniformly as just another value, as opposed to > treating it as an out-of-band condition, made `switch` more useful and made Java > code simpler and better. (For compatibility, a `switch` that has no `case null` > still throws `NullPointerException` when confronted with a null selector; we opt > into the new behavior with `case null`.) > ### Other switch tricks > The accumulation of new abilities for `switch` means that it can be used in more > situations than we might initially realize. One such use is replacing the > ternary conditional expression with boolean switch expressions; now that > `switch` can support boolean selectors, we can replace > expr ? A : B > with the switch expression > ``` > switch (expr) { > case true -> A; > case false -> B; > } > ``` > This might not immediately seem preferable, since the ternary expression is more > concise, but the `switch` is surely more clear. And, if we nest ternaries in > the arms of other ternaries (possibly deeply), this can quickly become > unreadable, whereas the corresponding nested switch remains readable even if > nested to several levels. We don't expect people to go out and change all their > ternaries to switches overnight, but we do expect that people will increasingly > find uses where a boolean switch is preferable to a ternary. (If the language > had boolean switch expressions from day 1, we might well not have had ternary > expressions at all.) > Another less-obvious example is using guards to do the selection, within the > bounds of the "pick one path" that `switch` is designed for. For example, we > can write the classic "FizzBuzz" exercise as: > ``` > String result = switch (getNumber()) { > case int i when i % 15 == 0 -> "FizzBuzz"; > case int i when i % 5 == 0 -> "Fizz"; > case int i when i % 3 == 0 -> "Buzz"; > case int i -> Integer.toString(i); > } > ``` > A more controversial use of the new-and-improved switch is as a replacement for > block expressions. Sometimes we want to use an expression (such as when passing > a parameter to a method), but the value can only be constructed using > statements: > ``` > String[] choices = new String[2]; > choices[0] = f(0); > choices[1] = f(1); > m(choices); > ``` > While it is somewhat "off label", we can replace this with a switch expression: > ``` > m(switch (0) { > default -> { > String[] choices = new String[2]; > choices[0] = f(0); > choices[1] = f(1); > yield choices; > } > }) > ``` > While these were not the primary use cases we had in mind when upgrading > `switch`, it illustrates how the combination of improvements to `switch` have > made it a sort of "swiss army knife". > ## Handling failure uniformly > Previously, null selector values were treated as out-of-band events, requiring > that users handle null selectors in a non-uniform way. The improvements to > `switch` in Java 21 enable null to be handled uniformly as a selector value, as > just another value. > A similar source of out-of-band events in `switch` is exceptions; if evaluating > the selector throws an exception, the switch immediately completes with that > exception. This is an entirely justifiable design choice, but it forces users > to handle exceptions using a separate mechanism, often a cumbersome one, just as > we did with null selectors: > ``` > Number parseNumber(String s) throws NumberFormatException() { ... } > try { > switch (parseNumber(input)) { > case Integer i -> handleInt(i); > case Float f -> handleFloat(f); > ... > } > } > catch (NumberFormatException e) { > ... handle exception ... > } > ``` > This is already unfortunate, as switch is designed to handle "choose one path > based on evaluating the selector", and "parse error" is one of the possible > consequences of evaluating the selector. It would be nice to be able to handle > error cases uniformly with success cases, as we did with null. Worse, this code > doesn't even mean what we want: the `catch` block catches not only exceptions > thrown by evaluating the selector, but also by the body of the switch. To say > what we mean, we need the even more unfortunate > ``` > var answer = null; > try { > answer = parseNumber(input); > } > catch (NumberFormatException e) { > ... handle exception ... > } > if (answer != null) { > switch (answer) { > case Integer i -> handleInt(i); > case Float f -> handleFloat(f); > ... > } > } > ``` > Just as it was an improvement to handle `null` uniformly as just another > potential value of the selector expression, we can get a similar improvement by > handling normal and exceptional completion uniformly as well. Normal and > exceptional completion are mutually exclusive, and the handling of exceptions in > `try-catch` already has a great deal in common with handling normal values in > `switch` statements (a catch clause is effectively matching to a type pattern.) > For activities with anticipated failure modes, handling successful completion > via one mechanism and failed completion through another makes code harder to > read and maintain. > ## Proposal > We can extend `switch` to handle exceptions more uniformly in a similar was as > we extended it to handle nulls by introducing `throws` cases, which match when > evaluating the selector expression completes abruptly with a compatible > exception: > ``` > String allTheLines = switch (Files.readAllLines(path)) { > case List lines -> lines.stream().collect(Collectors.joining("\n")); > case throws IOException e -> ""; > } > ``` > This captures the programmer's intent much more clearly, because the expected > success case and the expected failure case are handled uniformly and in the same > place, and their results can flow into the result of the switch expression. > The grammar of `case` labels is extended to include a new form, `case throws`, > which is followed by a type pattern: > case throws IOException e: > Exception cases can be used in all forms of `switch`: expression and statement > switches, switches that use traditional (colon) or single-consequence (arrow) > case labels. Exception cases can have guards like any other pattern case. I think I would prefer "case throws" to be spell "catch" even if we have to have a discussion about catch(Throwable t) vs catch Throwable t. > Exception cases have the obvious dominance order with other exception cases (the > same one used to validate order of `catch` clauses in `try-catch`), and do not > participate in dominance ordering with non-exceptional cases. It is a > compile-time error if an exception case specifies an exception type that cannot > be thrown by the selector expression, or a type that does not extend > `Throwable`. For clarity, exception cases should probably come after all other > non-exceptional cases. > When evaluating a `switch` statement or expression, the selector expression is > evaluated. If evaluation of the selector expression throws an exception, and > one of the exception cases in the `switch` matches the exception, then control > is transferred to the first exception case matching the exception. If no > exception case matches the exception, then the switch completes abruptly with > that same exception. I don't want to be the guy implementing this :) Your proposal is fighting against the physics of the VM, when you enter in an exception handler (a catch block), the whole stack disapear so you need some compiler magic to store everything which is on stack into locals before calling a switch expression so you can restore it inside the exception handler. I've implemented a very similar kind of transformation 10 years ago, it severly bloats the produced bytecode. Or maybe you are suggesting that this new switch will use a new bytecode construct (inline exception handler ?), it's more or less what JSR/RET was, but perhaps there is a better way. I understand the appeal of such construct, it's a way to switch (eheh) from a world with exceptions to a more functional world where errors are values. regards, R?mi -------------- next part -------------- An HTML attachment was scrubbed... URL: From archie.cobbs at gmail.com Tue Dec 12 22:58:59 2023 From: archie.cobbs at gmail.com (Archie Cobbs) Date: Tue, 12 Dec 2023 16:58:59 -0600 Subject: Effect cases in switch In-Reply-To: <327307323.80228032.1702420883408.JavaMail.zimbra@univ-eiffel.fr> References: <327307323.80228032.1702420883408.JavaMail.zimbra@univ-eiffel.fr> Message-ID: On Tue, Dec 12, 2023 at 4:41?PM Remi Forax wrote: > Your proposal is fighting against the physics of the VM, when you enter in > an exception handler (a catch block), the whole stack disapear so you need > some compiler magic to store everything which is on stack into locals > before calling a switch expression so you can restore it inside the > exception handler. I've implemented a very similar kind of transformation > 10 years ago, it severly bloats the produced bytecode. Or maybe you are > suggesting that this new switch will use a new bytecode construct (inline > exception handler ?), it's more or less what JSR/RET was, but perhaps there > is a better way. > What follows is a somewhat tangential point so don't let this hijack the core discussion.... In theory the compiler could "synthesize" subroutines, but this runs into verifier problems. See this thread which relates to JDK-8308023. So my tangential point is that perhaps this work, along with JDK-8308023, might motivate some thinking about how the verifier could be "smartened up" enough to allow the compiler to safely synthesize true subroutines. (Personally, something about JDK-8308023 is really offensive .. a 30 line program produces a 10MB class file.) -Archie -- Archie L. Cobbs -------------- next part -------------- An HTML attachment was scrubbed... URL: From brian.goetz at oracle.com Tue Dec 12 23:04:09 2023 From: brian.goetz at oracle.com (Brian Goetz) Date: Tue, 12 Dec 2023 18:04:09 -0500 Subject: Effect cases in switch In-Reply-To: <327307323.80228032.1702420883408.JavaMail.zimbra@univ-eiffel.fr> References: <327307323.80228032.1702420883408.JavaMail.zimbra@univ-eiffel.fr> Message-ID: > > Exception cases can be used in all forms of `switch`: expression > and statement > switches, switches that use traditional (colon) or > single-consequence (arrow) > case labels.? Exception cases can have guards like any other > pattern case. > > > I think I would prefer "case throws" to be spell "catch" even if we > have to have a discussion about catch(Throwable t) vs catch Throwable t. Ah, you've stepped in the trap.? You know the rule: if you make a syntax comment, you've implicitly signed up for all the semantics. Glad to know you're on board :) We knew that someone (everyone?) would ask about this, because its kind of the obvious choice.? But having thought about it for quite a while, I'm firmly convinced that this is in the "obvious but wrong" department.? PLEASE, let's not have a back-and-forth on this, because I don't want a substantive discussion to be derailed by a syntax triviality (we can return to this topic after the substantitive discussions have played out), but I will summarize some of the reasons why this is not what we want, because I think they are relevant to the goals of the feature: ?- The semantics would be extremely confusing, because a `case throws` matches only exceptions thrown _in evaluating the selector_, just like other cases match the result of a successful evaluation of the selector.? But if we expressed this as `switch ... catch`, users would forever be assuming, incorrectly, that they mean to be catching all errors from the switch block, including both those from the selector and those from the body.? That is *not* what this construct is about. ?- The *whole point* of this feature is allowing evaluation failures to be handled consistently and uniformly with successful evaluation.? Having a different syntax for handling failures does not help and does not highlight the uniformity.? Again, that is not what this constructs is about. The keyword `catch` is familiar but that is a very short-lived benefit.? The purpose of the feature is to allow uniform handling of results and effects; the syntax should reflect that. > > > > Exception cases have the obvious dominance order with other > exception cases (the > same one used to validate order of `catch` clauses in > `try-catch`), and do not > participate in dominance ordering with non-exceptional cases.? It is a > compile-time error if an exception case specifies an exception > type that cannot > be thrown by the selector expression, or a type that does not extend > `Throwable`.? For clarity, exception cases should probably come > after all other > non-exceptional cases. > > When evaluating a `switch` statement or expression, the selector > expression is > evaluated.? If evaluation of the selector expression throws an > exception, and > one of the exception cases in the `switch` matches the exception, > then control > is transferred to the first exception case matching the > exception.? If no > exception case matches the exception, then the switch completes > abruptly with > that same exception. > > > I don't want to be the guy implementing this :) Good news, Jan has volunteered to be that guy :) > I understand the appeal of such construct, it's a way to switch (eheh) > from a world with exceptions to a more functional world where errors > are values. > And even more so, to allow existing effectful APIs (like Future::get) to be consumed as uniformly as they are implemented. -------------- next part -------------- An HTML attachment was scrubbed... URL: From daniel.smith at oracle.com Wed Dec 13 00:27:24 2023 From: daniel.smith at oracle.com (Dan Smith) Date: Wed, 13 Dec 2023 00:27:24 +0000 Subject: Field initialization before 'super' Message-ID: In Valhalla we've been building on the language changes in JEP 447 (Statements Before Super) to move towards a more safe and reliable programming pattern for initializing final fields. Some of these ideas could make their way into the next iteration of Statements Before Super, to be further augmented with Value Classes (JEP 401). Two key observations: - Inside methods, the JVM allows writes to instance fields of "uninitialized" objects, before the 'super()' call. (In fact, javac has long used this capability to initialize fields that store captured state of inner classes.) - When a 'final' field is written before the 'super()' call, it is impossible to observe the field prior to its initialization. Thus, the field can be treated as truly immutable?every 'getfield' on the same instance will return the same value. (In contrast, in existing usage, uses of final fields may observe mutation if the object might still be under construction.) To enable and take advantage of early field initialization, we've envisioned the following changes: 1) As an exception to the general rule about 'this' usage, a "pre-construction context" allows writes to blank instance fields of the class. (The terminology may need updating, since you're clearly "constructing" the object if you're writing to its fields.) The fields are "write-only" at this stage?you can write into them but can't read them back. The regular DA/DU rules apply for final fields: they must be initialized exactly once by an initializer or by every 'super()'-calling constructor, whether in the prologue or the epilogue. At a 'this()' call, all final fields must be DU (because the delegated constructor will perform its own writes). No such restriction is needed for non-final fields; but it's an open question whether we should prohibit all writes before 'this()' anyway. Writes to non-final fields with initializers are disallowed, to avoid confusion about sequencing (the field initializer will always run later, overwriting whatever you put in the constructor prologue.) 2) If a final field is written before 'super()' via every constructor in the class, it can be considered a "strict final" field. It will never be observed to mutate. In the class file, ACC_STRICT is repurposed to indicate a strict final field. javac is responsible for identifying strict final fields. Existing early-initialized capture fields can probably be automatically counted as strict finals. ACC_STRICT implies ACC_FINAL and !ACC_STATIC. Verification ensures that a 'putfield' for an ACC_STRICT field of the current class never occurs after the 'super()' call. (Specifically, the receiver type for the putfield must be 'uninitializedThis', not a class type.) 3) Immutability of strict finals is a strong guarantee. JVM internals may treat strict final fields as truly immutable, without supporting any deopt paths when unexpected mutation occurs. The 'Field.setAccessible' method, which provides a standard API mechanism for mutating final fields, considers strict finals to be "non-modifiable", and will not enable reflective writes. (It already does the same for record fields.) Standard deserialization ensures strict finals are set, and so their values deserialized, before the object under construction is leaked to any user code. This probably means back references to an object from its own strict final fields are unsupported, and deserialize to 'null'. (Records already behave in this way.) Unsafe and JNI are capable of performing arbitrary, type-unsafe modifications to field storage. Clients who modify strict finals do so at their own risk, and JVM optimizations won't try to account for such usage. ----- That covers "phase 1" for this feature. Eventually, we'll want to address questions like - What about fields with initializers? - Can I have my implicit 'super()' call go at the end of my constructor? - Can javac check for me that my fields are strict? These sorts of capabilities probably make sense to introduce with value classes, and perhaps retrofit on records. Further design work needed to figure out how to release them for general consumption. All of that can be considered "phase 2", to come later. But for Statements Before Super, we're just proposing to start with (1), (2), and (3). I realize (2) and especially (3) are stretching the original concept of this JEP (which was purely language/compiler-oriented). But I think, from end users' perspective, it will all feel like the same feature. If wanted, though, I could see doing those pieces in their own JEP in parallel with Statements Before Super. From scolebourne at joda.org Wed Dec 13 08:52:14 2023 From: scolebourne at joda.org (Stephen Colebourne) Date: Wed, 13 Dec 2023 08:52:14 +0000 Subject: Effect cases in switch In-Reply-To: References: Message-ID: >From my perspective: Semantics: Good Syntax: Good (`case throws` is good, `catch` is not) Surprise level: Medium to Low Key is that the syntax can be read and implicitly understood without developers requiring training. Stephen On Tue, 12 Dec 2023 at 21:23, Brian Goetz wrote: > > Based on some inspiration from OCaml, and given that the significant upgrades to switch so far position it to do a lot more than it could before, we've been exploring a further refinement of switch to incorporate failure handling as well. > > (I realize that this may elicit strong reactions from some, but please give it some careful thought before giving voice to those reactions.) From forax at univ-mlv.fr Wed Dec 13 12:49:22 2023 From: forax at univ-mlv.fr (forax at univ-mlv.fr) Date: Wed, 13 Dec 2023 13:49:22 +0100 (CET) Subject: Effect cases in switch In-Reply-To: References: <327307323.80228032.1702420883408.JavaMail.zimbra@univ-eiffel.fr> Message-ID: <1882982839.81146561.1702471762200.JavaMail.zimbra@univ-eiffel.fr> > From: "Brian Goetz" > To: "Remi Forax" > Cc: "amber-spec-experts" > Sent: Wednesday, December 13, 2023 12:04:09 AM > Subject: Re: Effect cases in switch >>> Exception cases can be used in all forms of `switch`: expression and statement >>> switches, switches that use traditional (colon) or single-consequence (arrow) >>> case labels. Exception cases can have guards like any other pattern case. >> I think I would prefer "case throws" to be spell "catch" even if we have to have >> a discussion about catch(Throwable t) vs catch Throwable t. > Ah, you've stepped in the trap. You know the rule: if you make a syntax comment, > you've implicitly signed up for all the semantics. Glad to know you're on board > :) > We knew that someone (everyone?) would ask about this, because its kind of the > obvious choice. But having thought about it for quite a while, I'm firmly > convinced that this is in the "obvious but wrong" department. PLEASE, let's not > have a back-and-forth on this, because I don't want a substantive discussion to > be derailed by a syntax triviality (we can return to this topic after the > substantitive discussions have played out), but I will summarize some of the > reasons why this is not what we want, because I think they are relevant to the > goals of the feature: > - The semantics would be extremely confusing, because a `case throws` matches > only exceptions thrown _in evaluating the selector_, just like other cases > match the result of a successful evaluation of the selector. But if we > expressed this as `switch ... catch`, users would forever be assuming, > incorrectly, that they mean to be catching all errors from the switch block, > including both those from the selector and those from the body. That is *not* > what this construct is about. > - The *whole point* of this feature is allowing evaluation failures to be > handled consistently and uniformly with successful evaluation. Having a > different syntax for handling failures does not help and does not highlight the > uniformity. Again, that is not what this constructs is about. > The keyword `catch` is familiar but that is a very short-lived benefit. The > purpose of the feature is to allow uniform handling of results and effects; the > syntax should reflect that. I do not like case throws. I see why you like it, you want to do a case but not on the value of the expression but on the exception raised from the exception. It has also the advantage of being clears that a "case throw" can not catch exception like MatchingException that are. throws while executing a matching. But syntactically, if a "case throws" throws an exception, we have throws and throw on the same line, which is just awful to read case throws Exception e -> throw new AppException(e); Also it does not convey the right semantics, a "case throws" is not a "case", you can not mix it with the other cases and the merging of exceptions does not works like the merging of values, Some like case throws RuntimeException | Error e -> ... should be valid. For now, i think that __rescue is a better keyword, because unlike case and catch it does not have any existing semantics attached. >>> Exception cases have the obvious dominance order with other exception cases (the >>> same one used to validate order of `catch` clauses in `try-catch`), and do not >>> participate in dominance ordering with non-exceptional cases. It is a >>> compile-time error if an exception case specifies an exception type that cannot >>> be thrown by the selector expression, or a type that does not extend >>> `Throwable`. For clarity, exception cases should probably come after all other >>> non-exceptional cases. >>> When evaluating a `switch` statement or expression, the selector expression is >>> evaluated. If evaluation of the selector expression throws an exception, and >>> one of the exception cases in the `switch` matches the exception, then control >>> is transferred to the first exception case matching the exception. If no >>> exception case matches the exception, then the switch completes abruptly with >>> that same exception. >> I don't want to be the guy implementing this :) > Good news, Jan has volunteered to be that guy :) Are you sure you do not want a VM guy too ? >> I understand the appeal of such construct, it's a way to switch (eheh) from a >> world with exceptions to a more functional world where errors are values. > And even more so, to allow existing effectful APIs (like Future::get) to be > consumed as uniformly as they are implemented. Future::get as several issues, for me, the main one is that Callable/Future does not tracks the exception type so the cause of an ExecutionException is a Throwable so you are required to do a pattern-matching on the cause to repropagate at least the Error correctly. That said, exceptions are a good candidates for a deconstructor, i'm sure people will want to write switch (future.get()) { ... __rescue ExecutionException(Error error) -> throw error; } --- Also, I believe we are in trouble if the expression of the switch is typed as AutoCloseable, var paths = switch(Files.list(path)) { Stream stream -> yield stream.toList(); __rescue IOException _ -> List.of(); }; because users will want the switch to call close() on an AutoCloseable but it's not a backward compatible change. regards, R?mi -------------- next part -------------- An HTML attachment was scrubbed... URL: From jan.lahoda at oracle.com Wed Dec 13 14:15:17 2023 From: jan.lahoda at oracle.com (Jan Lahoda) Date: Wed, 13 Dec 2023 15:15:17 +0100 Subject: Effect cases in switch In-Reply-To: <1882982839.81146561.1702471762200.JavaMail.zimbra@univ-eiffel.fr> References: <327307323.80228032.1702420883408.JavaMail.zimbra@univ-eiffel.fr> <1882982839.81146561.1702471762200.JavaMail.zimbra@univ-eiffel.fr> Message-ID: <537b54dc-ba5b-41f8-916b-fcc8ef4971e5@oracle.com> On 13. 12. 23 13:49, forax at univ-mlv.fr wrote: [snip] > > Exception cases have the obvious dominance order with > other exception cases (the > same one used to validate order of `catch` clauses in > `try-catch`), and do not > participate in dominance ordering with non-exceptional > cases.? It is a > compile-time error if an exception case specifies an > exception type that cannot > be thrown by the selector expression, or a type that does > not extend > `Throwable`.? For clarity, exception cases should probably > come after all other > non-exceptional cases. > > When evaluating a `switch` statement or expression, the > selector expression is > evaluated.? If evaluation of the selector expression > throws an exception, and > one of the exception cases in the `switch` matches the > exception, then control > is transferred to the first exception case matching the > exception.? If no > exception case matches the exception, then the switch > completes abruptly with > that same exception. > > > I don't want to be the guy implementing this :) > > > Good news, Jan has volunteered to be that guy :) > > > Are you sure you do not want a VM guy too ? FWIW, we already run in the "try-catch with values on stack" problem in the case of a try-catch inside a switch expression. The current solution is to stash the stack to locals (which I suspect is similar to what you did in your case). It is not very pretty, but I think we can start with this and see if it poses problems in practice. (I am a bit more concerned about the switch-with-case-throws used as a conditional e.g. in an if - that may be tricky, as the code needs to have some particular shape. I think we have some ideas on how to implement that in the compiler, but didn't try them yet.) Jan -------------- next part -------------- An HTML attachment was scrubbed... URL: From pablogrisafi1975 at gmail.com Wed Dec 13 14:38:27 2023 From: pablogrisafi1975 at gmail.com (Pablo Grisafi) Date: Wed, 13 Dec 2023 11:38:27 -0300 Subject: yield in code blocks Message-ID: Recently in in the Effect cases in switch proposal Brian Goetz said the new switch can be used as a replacement for block expressions, writing something like ``` var x = switch (0) { default -> { String[] choices = new String[2]; var choice0 = f(0) choices[0] = choice0; choices[1] = f(1, choice0); yield choices; } } ``` (I slightly changed the original code to make the block more necessary) I wonder if Java could, as a small improvement, allow yield in any code block, so we could write ``` var x = { String[] choices = new String[2]; var choice0 = f(0) choices[0] = choice0; choices[1] = f(1, choice0); yield choices; } ``` Java already has blocks, and are almost never used, I suspect because you can't return a value from a block. They are a nice addition to the language, they are somewhere between a local function and a formatting option, they let you have restricted scope local variables which are only needed as intermediate values in the computation, neatly hidden from the rest of the program. I'm just suggesting to generalize the use of yield as a local return The complete Goetz quote: > A more controversial use of the new-and-improved switch is as a > replacement for > block expressions. Sometimes we want to use an expression (such as when > passing > a parameter to a method), but the value can only be constructed using > statements: > ``` > String[] choices = new String[2]; > choices[0] = f(0); > choices[1] = f(1); > m(choices); > ``` > While it is somewhat "off label", we can replace this with a switch > expression: > ``` > m(switch (0) { > default -> { > String[] choices = new String[2]; > choices[0] = f(0); > choices[1] = f(1); > yield choices; > } > }) > ``` Pablo Grisafi pablogrisafi1975 at gmail.com From forax at univ-mlv.fr Wed Dec 13 15:14:59 2023 From: forax at univ-mlv.fr (forax at univ-mlv.fr) Date: Wed, 13 Dec 2023 16:14:59 +0100 (CET) Subject: Effect cases in switch In-Reply-To: <537b54dc-ba5b-41f8-916b-fcc8ef4971e5@oracle.com> References: <327307323.80228032.1702420883408.JavaMail.zimbra@univ-eiffel.fr> <1882982839.81146561.1702471762200.JavaMail.zimbra@univ-eiffel.fr> <537b54dc-ba5b-41f8-916b-fcc8ef4971e5@oracle.com> Message-ID: <786876130.81298935.1702480499436.JavaMail.zimbra@univ-eiffel.fr> > From: "jan lahoda" > To: "Amber Expert Group Observers" , "Remi > Forax" , "Brian Goetz" > Sent: Wednesday, December 13, 2023 3:15:17 PM > Subject: Re: Effect cases in switch > On 13. 12. 23 13:49, [ mailto:forax at univ-mlv.fr | forax at univ-mlv.fr ] wrote: > [snip] >>>>> Exception cases have the obvious dominance order with other exception cases (the >>>>> same one used to validate order of `catch` clauses in `try-catch`), and do not >>>>> participate in dominance ordering with non-exceptional cases. It is a >>>>> compile-time error if an exception case specifies an exception type that cannot >>>>> be thrown by the selector expression, or a type that does not extend >>>>> `Throwable`. For clarity, exception cases should probably come after all other >>>>> non-exceptional cases. >>>>> When evaluating a `switch` statement or expression, the selector expression is >>>>> evaluated. If evaluation of the selector expression throws an exception, and >>>>> one of the exception cases in the `switch` matches the exception, then control >>>>> is transferred to the first exception case matching the exception. If no >>>>> exception case matches the exception, then the switch completes abruptly with >>>>> that same exception. >>>> I don't want to be the guy implementing this :) >>> Good news, Jan has volunteered to be that guy :) >> Are you sure you do not want a VM guy too ? > FWIW, we already run in the "try-catch with values on stack" problem in the case > of a try-catch inside a switch expression. The current solution is to stash the > stack to locals (which I suspect is similar to what you did in your case). It > is not very pretty, but I think we can start with this and see if it poses > problems in practice. I tried to be clever which is in retrospect may have not be not a good idea, i.e. not stash constants and things that were already in a local variable and effectively final. Anyway, the main issue I had (it was 10 years ago) was c1 deciding to not inline the method anymore because of the code bloat with as a direct consequence a reference on the stack that was previously GCed that was not anymore (because the interpreter was now keeping a reference in the locals). > (I am a bit more concerned about the switch-with-case-throws used as a > conditional e.g. in an if - that may be tricky, as the code needs to have some > particular shape. I think we have some ideas on how to implement that in the > compiler, but didn't try them yet.) > Jan R?mi -------------- next part -------------- An HTML attachment was scrubbed... URL: From forax at univ-mlv.fr Wed Dec 13 16:01:18 2023 From: forax at univ-mlv.fr (Remi Forax) Date: Wed, 13 Dec 2023 17:01:18 +0100 (CET) Subject: Field initialization before 'super' In-Reply-To: References: Message-ID: <2008829048.81354627.1702483278884.JavaMail.zimbra@univ-eiffel.fr> ----- Original Message ----- > From: "daniel smith" > To: "amber-spec-experts" > Sent: Wednesday, December 13, 2023 1:27:24 AM > Subject: Field initialization before 'super' > In Valhalla we've been building on the language changes in JEP 447 (Statements > Before Super) to move towards a more safe and reliable programming pattern for > initializing final fields. Some of these ideas could make their way into the > next iteration of Statements Before Super, to be further augmented with Value > Classes (JEP 401). > > Two key observations: > > - Inside methods, the JVM allows writes to instance fields of > "uninitialized" objects, before the 'super()' call. (In fact, javac has long > used this capability to initialize fields that store captured state of inner > classes.) > > - When a 'final' field is written before the 'super()' call, it is impossible to > observe the field prior to its initialization. Thus, the field can be treated > as truly immutable?every 'getfield' on the same instance will return the same > value. (In contrast, in existing usage, uses of final fields may observe > mutation if the object might still be under construction.) > > To enable and take advantage of early field initialization, we've envisioned the > following changes: > > 1) As an exception to the general rule about 'this' usage, a "pre-construction > context" allows writes to blank instance fields of the class. (The terminology > may need updating, since you're clearly "constructing" the object if you're > writing to its fields.) The fields are "write-only" at this stage?you can write > into them but can't read them back. > > The regular DA/DU rules apply for final fields: they must be initialized exactly > once by an initializer or by every 'super()'-calling constructor, whether in > the prologue or the epilogue. > > At a 'this()' call, all final fields must be DU (because the delegated > constructor will perform its own writes). No such restriction is needed for > non-final fields; but it's an open question whether we should prohibit all > writes before 'this()' anyway. I would say, keep that restriction. Having code before this(...) or even after this(...) is usually when you start to have fields that are written several times making the initialization steps hard to follow. > > Writes to non-final fields with initializers are disallowed, to avoid confusion > about sequencing (the field initializer will always run later, overwriting > whatever you put in the constructor prologue.) This seems to suggest that final fields with initializer should be written before the call to super() by default even if it's not a backward compatible change. > > 2) If a final field is written before 'super()' via every constructor in the > class, it can be considered a "strict final" field. It will never be observed > to mutate. I would be more "strict", if a final field is written before super() in one of the constructor, it should be written before super() in the other constructors. > > In the class file, ACC_STRICT is repurposed to indicate a strict final field. > javac is responsible for identifying strict final fields. Existing > early-initialized capture fields can probably be automatically counted as > strict finals. > > ACC_STRICT implies ACC_FINAL and !ACC_STATIC. Verification ensures that a > 'putfield' for an ACC_STRICT field of the current class never occurs after the > 'super()' call. (Specifically, the receiver type for the putfield must be > 'uninitializedThis', not a class type.) Before reusing STRICT, do we need a release were using STRICT is considered as an error, because currently, STRICT is just ignored ? > > 3) Immutability of strict finals is a strong guarantee. JVM internals may treat > strict final fields as truly immutable, without supporting any deopt paths when > unexpected mutation occurs. > > The 'Field.setAccessible' method, which provides a standard API mechanism for > mutating final fields, considers strict finals to be "non-modifiable", and will > not enable reflective writes. (It already does the same for record fields.) yes, and all synthetic fields (from capture) should be strict, because currently captured fields in lambda are like strict but captured fields is inner classes are like normal fields. > > Standard deserialization ensures strict finals are set, and so their values > deserialized, before the object under construction is leaked to any user code. > This probably means back references to an object from its own strict final > fields are unsupported, and deserialize to 'null'. (Records already behave in > this way.) > > Unsafe and JNI are capable of performing arbitrary, type-unsafe modifications to > field storage. Clients who modify strict finals do so at their own risk, and > JVM optimizations won't try to account for such usage. > > ----- > > That covers "phase 1" for this feature. Eventually, we'll want to address > questions like > - What about fields with initializers? If it's final, move them before super() and pray that it's a too incompatible with existing codes (at least we should try). > - Can I have my implicit 'super()' call go at the end of my constructor? I would like to avoid to allow non final fields to be initialized before super() so no. > - Can javac check for me that my fields are strict? That's the hope. For me the real issue is with fields with initializers, if it's too incompatible, we will have to introduce a new keyword "strict" for them, I hope we do not have to. > > These sorts of capabilities probably make sense to introduce with value classes, > and perhaps retrofit on records. Further design work needed to figure out how > to release them for general consumption. All of that can be considered "phase > 2", to come later. +1 for retrofitting records and lambda proxies. > > But for Statements Before Super, we're just proposing to start with (1), (2), > and (3). > > I realize (2) and especially (3) are stretching the original concept of this JEP > (which was purely language/compiler-oriented). But I think, from end users' > perspective, it will all feel like the same feature. If wanted, though, I could > see doing those pieces in their own JEP in parallel with Statements Before > Super. regards, R?mi From archie.cobbs at gmail.com Wed Dec 13 16:29:27 2023 From: archie.cobbs at gmail.com (Archie Cobbs) Date: Wed, 13 Dec 2023 10:29:27 -0600 Subject: Field initialization before 'super' In-Reply-To: <2008829048.81354627.1702483278884.JavaMail.zimbra@univ-eiffel.fr> References: <2008829048.81354627.1702483278884.JavaMail.zimbra@univ-eiffel.fr> Message-ID: On Wed, Dec 13, 2023 at 10:02?AM Remi Forax wrote: > > No such restriction is needed for non-final fields; but it's an open > question > > whether we should prohibit all writes before 'this()' anyway. > > I would say, keep that restriction. > I tend to agree. One benefit would be it creates more separation between final fields and non-final fields, which would help encourage people to make fields final so they get all the benefits. > > Writes to non-final fields with initializers are disallowed, to avoid > confusion > > about sequencing (the field initializer will always run later, > overwriting > > whatever you put in the constructor prologue.) > > This seems to suggest that final fields with initializer should be written > before the call to super() by default even if it's not a backward > compatible change. > That could only work if the initializer doesn't read "this"... which means only some initializers would qualify, which would be confusing. > captured fields in lambda are like strict but captured fields is inner > classes are like normal fields. > I was curious why this would be true and it appears it's not true, at least from this test: $ cat Test.java public class Test { public Test(String x) { new Runnable() { @Override public void run() { System.out.println(x); } }.run(); } } $ javap -c -classpath classes Test\$1 Compiled from "Test.java" class Test$1 implements java.lang.Runnable { final java.lang.String val$x; Test$1(); Code: 0: aload_0 1: aload_2 2: putfield #1 // Field val$x:Ljava/lang/String; 5: aload_0 6: invokespecial #7 // Method java/lang/Object."":()V 9: return ... You can see in the Test$1 constructor Test$1.x is being initialized prior to super(). > - Can I have my implicit 'super()' call go at the end of my constructor? > My opinion is also "no" on this question... but because it would add another logical fork in developers' minds for too little benefit. It's easy enough to just put it in there explicitly. -Archie -- Archie L. Cobbs -------------- next part -------------- An HTML attachment was scrubbed... URL: From forax at univ-mlv.fr Wed Dec 13 21:07:20 2023 From: forax at univ-mlv.fr (forax at univ-mlv.fr) Date: Wed, 13 Dec 2023 22:07:20 +0100 (CET) Subject: Field initialization before 'super' In-Reply-To: References: <2008829048.81354627.1702483278884.JavaMail.zimbra@univ-eiffel.fr> Message-ID: <244280924.81515287.1702501640695.JavaMail.zimbra@univ-eiffel.fr> > From: "Archie Cobbs" > To: "Remi Forax" > Cc: "daniel smith" , "amber-spec-experts" > > Sent: Wednesday, December 13, 2023 5:29:27 PM > Subject: Re: Field initialization before 'super' > On Wed, Dec 13, 2023 at 10:02 AM Remi Forax < [ mailto:forax at univ-mlv.fr | > forax at univ-mlv.fr ] > wrote: >> > No such restriction is needed for non-final fields; but it's an open question >> > whether we should prohibit all writes before 'this()' anyway. >> I would say, keep that restriction. > I tend to agree. One benefit would be it creates more separation between final > fields and non-final fields, which would help encourage people to make fields > final so they get all the benefits. >> > Writes to non-final fields with initializers are disallowed, to avoid confusion >> > about sequencing (the field initializer will always run later, overwriting >> > whatever you put in the constructor prologue.) >> This seems to suggest that final fields with initializer should be written >> before the call to super() by default even if it's not a backward compatible >> change. > That could only work if the initializer doesn't read "this"... which means only > some initializers would qualify, which would be confusing. yes, >> captured fields in lambda are like strict but captured fields is inner classes >> are like normal fields. > I was curious why this would be true and it appears it's not true, at least from > this test: > $ cat Test.java > public class Test { > public Test(String x) { > new Runnable() { > @Override > public void run() { > System.out.println(x); > } > }.run(); > } > } > $ javap -c -classpath classes Test\$1 > Compiled from "Test.java" > class Test$1 implements java.lang.Runnable { > final java.lang.String val$x; > Test$1(); > Code: > 0: aload_0 > 1: aload_2 > 2: putfield #1 // Field val$x:Ljava/lang/String; > 5: aload_0 > 6: invokespecial #7 // Method java/lang/Object."":()V > 9: return > ... > You can see in the Test$1 constructor Test$1.x is being initialized prior to > super(). Yes, but you can still change the value of the field by reflection, unlike with the fields of a lambda proxy, so the field containing the captured value is not strict. >> > - Can I have my implicit 'super()' call go at the end of my constructor? > My opinion is also "no" on this question... but because it would add another > logical fork in developers' minds for too little benefit. It's easy enough to > just put it in there explicitly. What we are trying to do here, is to follow Dan's idea that there is no need for an additional keyword and that the compiler can infer the strictness by itself. As you said, the problem of that idea is that the developer is not really in control, subtle changes like the initializer depending on 'this' make the final field strict or not. > -Archie regards, R?mi > -- > Archie L. Cobbs -------------- next part -------------- An HTML attachment was scrubbed... URL: From archie.cobbs at gmail.com Wed Dec 13 21:19:56 2023 From: archie.cobbs at gmail.com (Archie Cobbs) Date: Wed, 13 Dec 2023 15:19:56 -0600 Subject: Field initialization before 'super' In-Reply-To: <244280924.81515287.1702501640695.JavaMail.zimbra@univ-eiffel.fr> References: <2008829048.81354627.1702483278884.JavaMail.zimbra@univ-eiffel.fr> <244280924.81515287.1702501640695.JavaMail.zimbra@univ-eiffel.fr> Message-ID: On Wed, Dec 13, 2023 at 3:07?PM wrote: > > captured fields in lambda are like strict but captured fields is inner >> classes are like normal fields. >> > > I was curious why this would be true and it appears it's not true, at > least from this test: > > > Yes, but you can still change the value of the field by reflection, unlike > with the fields of a lambda proxy, so the field containing the captured > value is not strict. > Thanks for the clarification, I missed that piece of it. > My opinion is also "no" on this question... but because it would add > another logical fork in developers' minds for too little benefit. It's easy > enough to just put it in there explicitly. > > > What we are trying to do here, is to follow Dan's idea that there is no > need for an additional keyword and that the compiler can infer the > strictness by itself. > As you said, the problem of that idea is that the developer is not really > in control, subtle changes like the initializer depending on 'this' make > the final field strict or not. > So it would boil down to the difference between: private final int x; // not strict Foo(int x) { // implicit super() goes here this.x = x; } vs. private final int x; // implicitly strict Foo(int x) { this.x = x; // implicit super() goes here } which only differ in behavior in the introspection and serialization domains. Thanks - now I get it. -Archie -- Archie L. Cobbs -------------- next part -------------- An HTML attachment was scrubbed... URL: From john.r.rose at oracle.com Wed Dec 13 22:36:24 2023 From: john.r.rose at oracle.com (John Rose) Date: Wed, 13 Dec 2023 14:36:24 -0800 Subject: Field initialization before 'super' In-Reply-To: References: Message-ID: <82996BA6-6F5D-4C50-A054-C7A6DEF8D11B@oracle.com> I have been wanting this feature for literally decades! I?m glad to see that its time is coming. My reasons: - users can prove their code is free of certain init-races and NPEs - specifically, safe publication is safer - JIT no longer needs to do escape analysis to trust finals - stuff we had to do for ICs and lambdas is now generally available - we get closer to fixing certain race conditions in deserialization - we get closer to dealing with tech. debt from setAccessible (The cost of this feature is cultural. Now users have two ways to do one task, initialize a final field: The improved way, and the usually-OK-but-sometimes-not way from literally Java 1.1. Which I defined. Which is why I?ve been wanting a fix for years.) I think it all works as you have framed it, Dan, with phase 1 quietly inferring strictness from constructor behavior. The user must opt into the changed field semantics by changing constructor code. There is no ?nicer? way to opt into strictness, say on a whole-class basis, but that is clearly something that can follow later. The ACC_STRICT bit reflects the internal implementation change in the order of construction. Note: This change is not an API change, yet. Remember that reflection can observe private details like ACC_BRIDGE. (It may become an API change (with more explicit declaration) when we start interacting with Valhalla value classes. Spoiler: A value class, and all its supers, must have only strict final fields.) Even if we didn?t have ACC_STRICT in the classfile, the JVM could infer its setting by examining all relevant constructors. But it seems fair to put the bit into the classfile if the user goes to the trouble if refactoring construction order. (It cannot be inferred from legacy code, since the putfields are in the wrong places.) And for Valhalla we really do need ACC_STRICT, along with the related verifier rule to give it teeth. > ACC_STRICT implies ACC_FINAL and !ACC_STATIC. Verification ensures > that a 'putfield' for an ACC_STRICT field of the current class never > occurs after the 'super()' call. (Specifically, the receiver type for > the putfield must be 'uninitializedThis', not a class type.) Mentioning this entanglement between mode bits is a nerd-snipe. Let?s be careful here, for the sake of the future, that we don?t rule out strict statics, and strict non-finals. What I think we can and should do is roll out strictness ONLY for final non-statics, but reserve judgement about the other combinations. The combination of guarantees from (a) the existing language rules for finals and from (b) the new rules for strictness makes the following statement true for strict finals: >> NO-READ-BEFORE-INIT It is impossible for the JVM to issue a read of >> a strict field until after the first write. This is a broader statement than the fiddly details of strict finals. But it is equivalent, given (a) and (b), for non-static finals. (Prove me wrong!) Curiously, this statement could, in the future, be a contract for other kinds of fields besides non-static finals. And that is how nerd-snipes go viral. (And to my fellow nerds: PLEASE do not burden this JEP with all of the possible implications of that formulation of strictness. All in good time.) > 3) Immutability of strict finals is a strong guarantee. JVM internals > may treat strict final fields as truly immutable, without supporting > any deopt paths when unexpected mutation occurs. > > The 'Field.setAccessible' method, which provides a standard API > mechanism for mutating final fields, considers strict finals to be > "non-modifiable", and will not enable reflective writes. (It already > does the same for record fields.) There is some debt here to pay, because some framework authors will ask what is the alternative to the setAccessible we have taken away. But I think that can come later. One way to soften the blow would be to let the framework authors down more gently, and give some story for ?well we allowed it up to now, and we will throw warnings and then errors at you soon, and you should learn the alternative tactics real soon?. The Java module system played these games, as does Panama in its native method access rules. But saying ?it is turned off? is a start. The start of a conversation. What happens if (somehow) a framework author starts monkeying with final fields? This breaks the unique initialization condition for finals, since any final smashed by a framework presumably already had a perfectly legitimate value assigned by its constructor. (Or if it was Unsafe.allocateInstance, the legitimate value was the default value.) Really, such frameworks create potential violations of the NO-READ-BEFORE-INIT rule I stated above. By the way, the optimizing JIT secretly performs reads of fields, when it inspects live data and decides to constant fold it, or make other conclusions based on the live data. So even if the source code does not apparently commit read-before-init faults, the JIT might. It?s the JIT?s responsibility to be careful with such reads. One thing we might want to do is to add logic to setAccessible that throws away code that might have been optimized too confidently, since setAccessible(true) means there may be read-before-init hazards coming, on a particular field. This sort of thing has been prototyped in the past more than once, I think. It?s just hard to put the whole story together without the early-init of fields given by this JEP. But the easiest best starting point is to turn off setAccessible as much as possible. > Standard deserialization ensures strict finals are set, and so their > values deserialized, before the object under construction is leaked to > any user code. This probably means back references to an object from > its own strict final fields are unsupported, and deserialize to > 'null'. (Records already behave in this way.) My nightmare about this has always been: Can I prove that the JIT will NEVER peek into an unfinished object, and draw conclusions from the uninitialized field contents? Such a proof seems tricky, given that the deserialization code makes no clear distinction (that the JIT can see) between under-construction objects and safe-to-publish objects. This is a long-standing technical debt in deserialization (all such frameworks). > Unsafe and JNI are capable of performing arbitrary, type-unsafe > modifications to field storage. Clients who modify strict finals do so > at their own risk, and JVM optimizations won't try to account for such > usage. Yep! If you use Unsafe or JNI to hack objects, you become a VM implementor. Don?t want that responsibility? Use a higher level API. (Such as record canonical constructors or TBD for strict fields or Valhalla values.) > That covers "phase 1" for this feature. Eventually, we'll want to > address questions like > - What about fields with initializers? (Choices there include ?move the field initializers always?, ?move initializers which are simple enough?, and ?don?t use field initializers?. Dunno which combination wins.) > - Can I have my implicit 'super()' call go at the end of my > constructor? (Choices include, ?sure, why not? we?ll pretend the super doesn?t do side effects?, or ?we can somehow tell the super is safe?, or ?nope, only for Valhalla value classes?, or ?only if you opt in with a class-level keyword?. Again, who knows which one wins.) > - Can javac check for me that my fields are strict? (Choices include, ?request checks with a class-level keyword?, or ?only for Valhalla values?, or ?use an annotation?. Today I find it kind of charming that an annotation might do the trick, at least before Valhalla. Something like @Override that merely observes and comments. It might even comment that you could put your super in a better place.) ? John -------------- next part -------------- An HTML attachment was scrubbed... URL: From rschmitt at pobox.com Thu Dec 14 05:42:13 2023 From: rschmitt at pobox.com (Ryan Schmitt) Date: Wed, 13 Dec 2023 21:42:13 -0800 Subject: Effect cases in switch In-Reply-To: References: Message-ID: Overall, I really like this proposal. I don't think `try` should have a monopoly on controlling exception handling; unifying exceptional and non-exceptional control flow in a single syntactic construct has too many benefits for ergonomics and clarity, especially for checked exceptions. (It seems to me that something like the current proposal is what checked exceptions were originally "getting at.") One thing I'm interested in is the interaction (or lack thereof) between switch expressions and automatic resource management (better known as try-with-resources). Currently, `try` is the only construct that can control exception handling, but it's also the only construct that can auto-close resources, and sometimes these two functions seem frustratingly poorly integrated, as in this example: try { try (BufferedReader r = new BufferedReader(new InputStreamReader(Files.newInputStream(Path.of("lines"))))) { return r.readLine(); } } catch (IOException ex) { return ""; } The current proposal gives us an alternative to these nested `try` blocks: switch (new BufferedReader(new InputStreamReader(Files.newInputStream(Path.of("lines"))))) { case BufferedReader r -> try (r) { r.readLine() } case throws IOException _ -> "" } This is a bit odd, though, because we need a `switch` purely for exception handling, and we need a `try` that's doing *no* exception handling but is simply there for resource cleanup. Additionally, there's a superficial similarity between the try-with-resources syntax and the switch expression syntax, which may lead to confusion if the latter starts doing the job of the former. This code, for example, looks correct and familiar but actually has a resource leak: switch (new BufferedReader(new InputStreamReader(Files.newInputStream(Path.of("lines"))))) { case BufferedReader r -> r.readLine() case throws IOException _ -> "" } I point this out not to encourage syntax bikeshedding, but just to ask what role automatic resource management should play in this discussion. If `try` shouldn't have a monopoly on exception handling, maybe it shouldn't have a monopoly on automatic resource management either. On Tue, Dec 12, 2023 at 1:23?PM Brian Goetz wrote: > Based on some inspiration from OCaml, and given that the significant > upgrades to switch so far position it to do a lot more than it could > before, we've been exploring a further refinement of switch to incorporate > failure handling as well. > > (I realize that this may elicit strong reactions from some, but please > give it some careful thought before giving voice to those reactions.) > > > > # Uniform handling of failure in switch > > ## Summary > > Enhance the `switch` construct to support `case` labels that match > exceptions > thrown during evaluation of the selector expression, providing uniform > handling > of normal and exceptional results. > > ## Background > > The purpose of the `switch` construct is to choose a single course of > action > based on evaluating a single expression (the "selector"). The `switch` > construct is not strictly needed in the language; everything that `switch` > does > can be done by `if-else`. But the language includes `switch` because it > embodies useful constraints which both streamline the code and enable more > comprehensive error checking. > > The original version of `switch` was very limited: the selector expression > was > limited to a small number of primitive types, the `case` labels were > limited to > numeric literals, and the body of a switch was limited to operating by > side-effects (statements only, no expressions.) Because of these > limitations, > the use of `switch` was usually limited to low-level code such as parsers > and > state machines. In Java 5 and 7, `switch` received minor upgrades to > support > primitive wrapper types, enums, and strings as selectors, but its role as > "pick > from one of these constants" did not change significantly. > > Recently, `switch` has gotten more significant upgrades, to the point > where it > can take on a much bigger role in day-to-day program logic. Switch can > now be > used as an expression in addition to a statement, enabling greater > composition > and more streamlined code. The selector expression can now be any type. > The > `case` labels in a switch block can be rich patterns, not just constants, > and > have arbitrary predicates as guards. We get much richer type checking for > exhaustiveness when switching over selectors involving sealed types. Taken > together, this means much more program logic can be expressed concisely and > reliably using `switch` than previously. > > ### Bringing nulls into `switch` > > Historically, the `switch` construct was null-hostile; if the selector > evaluated > to `null`, the `switch` immediately completed abruptly with > `NullPointerException`. This made a certain amount of sense when the only > reference types that could be used in switch were primitive wrappers and > enums, > for which nulls were almost always indicative of an error, but as `switch` > became more powerful, this was increasingly a mismatch for what we wanted > to do > with `switch`. Developers were forced to work around this, but the > workarounds > had undesirable consequences (such as forcing the use of statement switches > instead of expression switches.) Previously, to handle null, one would > have to > separately evaluate the selector and compare it to `null` using `if`: > > ``` > SomeType selector = computeSelector(); > SomeOtherType result; > if (selector == null) { > result = handleNull(); > } > else { > switch (selector) { > case X: > result = handleX(); > break; > case Y: > result = handleY(); > break; > } > } > ``` > > Not only is this more cumbersome and less concise, but it goes against the > main > job of `switch`, which is streamline "pick one path based on a selector > expression" decisions. Outcomes are not handled uniformly, they are not > handled > in one place, and the inability to express all of this as an expression > limits > composition with other language features. > > In Java 21, it became possible to treat `null` as just another possible > value of > the selector in a `case` clause (and even combine `null` handling with > `default`), so that the above mess could reduce to > > ``` > SomeOtherType result = switch (computeSelector()) { > case null -> handleNull(); > case X -> handleX(); > case Y -> handleY(); > } > ``` > > This is simpler to read, less error-prone, and interacts better with the > rest of > the language. Treating nulls uniformly as just another value, as opposed > to > treating it as an out-of-band condition, made `switch` more useful and > made Java > code simpler and better. (For compatibility, a `switch` that has no `case > null` > still throws `NullPointerException` when confronted with a null selector; > we opt > into the new behavior with `case null`.) > > ### Other switch tricks > > The accumulation of new abilities for `switch` means that it can be used > in more > situations than we might initially realize. One such use is replacing the > ternary conditional expression with boolean switch expressions; now that > `switch` can support boolean selectors, we can replace > > expr ? A : B > > with the switch expression > > ``` > switch (expr) { > case true -> A; > case false -> B; > } > ``` > > This might not immediately seem preferable, since the ternary expression > is more > concise, but the `switch` is surely more clear. And, if we nest ternaries > in > the arms of other ternaries (possibly deeply), this can quickly become > unreadable, whereas the corresponding nested switch remains readable even > if > nested to several levels. We don't expect people to go out and change all > their > ternaries to switches overnight, but we do expect that people will > increasingly > find uses where a boolean switch is preferable to a ternary. (If the > language > had boolean switch expressions from day 1, we might well not have had > ternary > expressions at all.) > > Another less-obvious example is using guards to do the selection, within > the > bounds of the "pick one path" that `switch` is designed for. For example, > we > can write the classic "FizzBuzz" exercise as: > > ``` > String result = switch (getNumber()) { > case int i when i % 15 == 0 -> "FizzBuzz"; > case int i when i % 5 == 0 -> "Fizz"; > case int i when i % 3 == 0 -> "Buzz"; > case int i -> Integer.toString(i); > } > ``` > > A more controversial use of the new-and-improved switch is as a > replacement for > block expressions. Sometimes we want to use an expression (such as when > passing > a parameter to a method), but the value can only be constructed using > statements: > > ``` > String[] choices = new String[2]; > choices[0] = f(0); > choices[1] = f(1); > m(choices); > ``` > > While it is somewhat "off label", we can replace this with a switch > expression: > > ``` > m(switch (0) { > default -> { > String[] choices = new String[2]; > choices[0] = f(0); > choices[1] = f(1); > yield choices; > } > }) > ``` > > While these were not the primary use cases we had in mind when upgrading > `switch`, it illustrates how the combination of improvements to `switch` > have > made it a sort of "swiss army knife". > > ## Handling failure uniformly > > Previously, null selector values were treated as out-of-band events, > requiring > that users handle null selectors in a non-uniform way. The improvements to > `switch` in Java 21 enable null to be handled uniformly as a selector > value, as > just another value. > > A similar source of out-of-band events in `switch` is exceptions; if > evaluating > the selector throws an exception, the switch immediately completes with > that > exception. This is an entirely justifiable design choice, but it forces > users > to handle exceptions using a separate mechanism, often a cumbersome one, > just as > we did with null selectors: > > ``` > Number parseNumber(String s) throws NumberFormatException() { ... } > > try { > switch (parseNumber(input)) { > case Integer i -> handleInt(i); > case Float f -> handleFloat(f); > ... > } > } > catch (NumberFormatException e) { > ... handle exception ... > } > ``` > > This is already unfortunate, as switch is designed to handle "choose one > path > based on evaluating the selector", and "parse error" is one of the possible > consequences of evaluating the selector. It would be nice to be able to > handle > error cases uniformly with success cases, as we did with null. Worse, > this code > doesn't even mean what we want: the `catch` block catches not only > exceptions > thrown by evaluating the selector, but also by the body of the switch. To > say > what we mean, we need the even more unfortunate > > ``` > var answer = null; > try { > answer = parseNumber(input); > } > catch (NumberFormatException e) { > ... handle exception ... > } > > if (answer != null) { > switch (answer) { > case Integer i -> handleInt(i); > case Float f -> handleFloat(f); > ... > } > } > ``` > > Just as it was an improvement to handle `null` uniformly as just another > potential value of the selector expression, we can get a similar > improvement by > handling normal and exceptional completion uniformly as well. Normal and > exceptional completion are mutually exclusive, and the handling of > exceptions in > `try-catch` already has a great deal in common with handling normal values > in > `switch` statements (a catch clause is effectively matching to a type > pattern.) > For activities with anticipated failure modes, handling successful > completion > via one mechanism and failed completion through another makes code harder > to > read and maintain. > > ## Proposal > > We can extend `switch` to handle exceptions more uniformly in a similar > was as > we extended it to handle nulls by introducing `throws` cases, which match > when > evaluating the selector expression completes abruptly with a compatible > exception: > > ``` > String allTheLines = switch (Files.readAllLines(path)) { > case List lines -> > lines.stream().collect(Collectors.joining("\n")); > case throws IOException e -> ""; > } > ``` > > This captures the programmer's intent much more clearly, because the > expected > success case and the expected failure case are handled uniformly and in > the same > place, and their results can flow into the result of the switch expression. > > The grammar of `case` labels is extended to include a new form, `case > throws`, > which is followed by a type pattern: > > case throws IOException e: > > Exception cases can be used in all forms of `switch`: expression and > statement > switches, switches that use traditional (colon) or single-consequence > (arrow) > case labels. Exception cases can have guards like any other pattern case. > > > Exception cases have the obvious dominance order with other exception > cases (the > same one used to validate order of `catch` clauses in `try-catch`), and do > not > participate in dominance ordering with non-exceptional cases. It is a > compile-time error if an exception case specifies an exception type that > cannot > be thrown by the selector expression, or a type that does not extend > `Throwable`. For clarity, exception cases should probably come after all > other > non-exceptional cases. > > When evaluating a `switch` statement or expression, the selector > expression is > evaluated. If evaluation of the selector expression throws an exception, > and > one of the exception cases in the `switch` matches the exception, then > control > is transferred to the first exception case matching the exception. If no > exception case matches the exception, then the switch completes abruptly > with > that same exception. > > This slightly adjusts the set of exceptions thrown by a `switch`; if an > exception is thrown by the selector expression but not the body of the > switch, > and it is matched by an unguarded exception case, then the switch is not > considered to throw that exception. > > ### Examples > > In some cases, we will want to totalize a partial computation by supplying > a > fallback value when there is an exception: > > ``` > Function> safeParse = > s -> switch(Integer.parseInt(s)) { > case int i -> Optional.of(i); > case throws NumberFormatException _ -> Optional.empty(); > }; > ``` > > In other cases, we may want to ignore exceptional values entirely: > > ``` > stream.mapMulti((f, c) -> switch (readFileToString(url)) { > case String s -> c.accept(s); > case throws MalformedURLException _ -> { }; > }); > ``` > > In others, we may want to process the result of a method like `Future::get` > more uniformly: > > ``` > Future f = ... > switch (f.get()) { > case String s -> process(s); > case throws ExecutionException(var underlying) -> throw underlying; > case throws TimeoutException e -> cancel(); > } > ``` > > ### Discussion > > We expect the reaction to this to be initially uncomfortable, because > historically the `try` statement was the only way to control the handling > of > exceptions. There is clearly still a role for `try` in its full > generality, but > just as `switch` profitably handles a constrained subset of the situations > that > could be handled with the more general `if-else` construct, there is > similarly > profit in allowing it to handle a constrained subset of the cases handled > by the > more general `try-catch` construct. Specifically, the situation that > `switch` > is made for: evaluate an expression, and then choose one path based on the > outcome of evaluating that expression, applies equally well to > discriminating > unsuccessful evaluations. Clients will often want to handle exceptional > as well > as successful completion, and doing so uniformly within a single construct > is > likely to be clearer and less error-prone than spreading it over two > constructs. > > Java APIs are full of methods that can either produce a result or throw an > exception, such as `Future::get`. Writing APIs in this way is natural for > the > API author, because they get to handle computation in a natural way; if > they > get to the point where they do not want to proceed, they can `throw` an > exception, just as when they get to the point where the computation is > done, > they can `return` a value. Unfortunately, this convenience and uniformity > for > API authors puts an extra burden on API consumers; handling failures is > more > cumbersome than handling the successful case. Allowing clients to > `switch` over > all the ways a computation could complete heals this rift. > > None of this is to say that `try-catch` is obsolete, any more than `switch` > makes `if-else` obsolete. When we have a large block of code that may > fail at > multiple points, handling all the exceptions from the block together is > often > more convenient than handling each exception at its generation point. But > when > we scale `try-catch` down to a single expression, it can get awkward. The > effect is felt most severely with expression lambdas, which undergo a > significant syntactic expansion if they want to handle their own > exceptions. > > > -------------- next part -------------- An HTML attachment was scrubbed... URL: From john.r.rose at oracle.com Thu Dec 14 07:16:54 2023 From: john.r.rose at oracle.com (John Rose) Date: Wed, 13 Dec 2023 23:16:54 -0800 Subject: Effect cases in switch In-Reply-To: <327307323.80228032.1702420883408.JavaMail.zimbra@univ-eiffel.fr> References: <327307323.80228032.1702420883408.JavaMail.zimbra@univ-eiffel.fr> Message-ID: <90DE123B-224C-4B3D-BCDD-2966DF56A6AB@oracle.com> On 12 Dec 2023, at 14:41, Remi Forax wrote: > ? I don't want to be the guy implementing this :) > Your proposal is fighting against the physics of the VM, when you > enter in an exception handler (a catch block) With my VM hat on I wrote something on another thread about this. You can either split the switch or lift the expression to an Either record. So, I think that a switch with exception cases can be lowered by partitioning it into two switches, neither of which have exception cases. Then the two switches can be further lowered into an invokedynamic each, or something like that. If the case logic needs to be processed as a whole by one BSM call, maybe the two indys share a condy which contains the static runtime analysis of the switch metadata. Then each indy projects the data it needs out of the shared condy. Or, the expression result can be lifted to a 2-tuple of normal + exceptional results. I?m just throwing out options here, obviously? So, this: ``` ? = switch (E0) { case P01 -> E01; case P02 -> E02; ? case throws P10 -> E10; case throws P11 -> E11; ? }; ``` could lower to a two-switch shape like this: ``` final $T2 $V2; // temp to catch switch result $L2: for (;;) { // this block is breakable, maybe a one-execution for-loop final $T0 $V0; try { // very narrow catch region, just for E0 $V0 = E0; // normal exit falls through to normal (non-catching) cases (P01?) } catch ($T1 $V1) { // $T1 is union of all exceptions covered by P10? // adjusted normal (non-catching) switch handles all catching cases (P10?) $V2 = switch ($V1) { case P10 -> E10; case P11 -> E11; ? }; break $L2; // bytecode goto hops over non-throwing case logic } // normal (non-catching) switch handles normal cases (P01?) $V2 = switch ($V0) { case P01 -> E01; case P02 -> E02; ? }; break $L2; // bytecode fallthrough } // $V2 is now live from one of the two breaks from $L2 ? = $V2; ``` The actual post-case statements (Enn) could be split between the two switches, or else merged into a third switch; the two initial switches would export bindings (as an Object) and also a small integer case number, which would be the selector to the third switch, rendered with a simple tableswitch bytecode. It is not trivial to support all switch features simultaneously. The third tableswitch seems (to me) to be a required tactic to support fall-through between cases. It is probably overkill apart from that, although it is (to me as VM guy) appealing to funnel all the decision results through a tableswitch. Another way to think about this (there are many viable choices) is to lift the evaluation of the selector into a combination of two variables, a normal result and an exception, and then (somehow) run a single switch over the pair of values. ``` final $T1 $V1 = null; // temp to catch exceptional switch selector result final $T2 $V2 = null; // temp to catch normal switch selector result try { // very narrow catch region, just for E0 $V2 = (E0); } catch ($T1 $V1a) { // $T1 is union of all exceptions covered by P10? $V1 = $V1a; } ? = switch ($EITHER($V2, $V1)) { case $EITHER(P01, null) -> E01; ? case $EITHER(null, P10) -> E10; ? } ``` A Java programmer will need to ?box? the two values together as an Either record (which I like to call an ?ExBox?), but the javac engineer can probably get away with switching over an internal 2-tuple or Either type. Maybe just a 2-array? I guess I won?t want to do the javac engineering either, but I do think the ?lucky? engineer has multiple options. Fun stuff. ? John P.S. If we ever get in-args into patterns it gets even more wooly, since those would be little expressions embedded in random places in the case patterns, and would be evaluated at unpredictable moments by the synthetic switch decision logic. They probably need to be eta-converted (aka lambda-quoted) if they have any side-effects. For more amusement on eta-conversion/lambda-quoting, you might like this: https://cr.openjdk.org/~jrose/jls/eta-abstraction-expr-quoting.html -------------- next part -------------- An HTML attachment was scrubbed... URL: From brian.goetz at oracle.com Thu Dec 14 15:49:39 2023 From: brian.goetz at oracle.com (Brian Goetz) Date: Thu, 14 Dec 2023 10:49:39 -0500 Subject: Effect cases in switch In-Reply-To: <90DE123B-224C-4B3D-BCDD-2966DF56A6AB@oracle.com> References: <327307323.80228032.1702420883408.JavaMail.zimbra@univ-eiffel.fr> <90DE123B-224C-4B3D-BCDD-2966DF56A6AB@oracle.com> Message-ID: One of the many reasons in-args should be effectively final. On 12/14/2023 2:16 AM, John Rose wrote: > > P.S. If we ever get in-args into patterns it gets even more wooly, > since those would be little expressions embedded in random places > in the case patterns, and would be evaluated at unpredictable > moments by the synthetic switch decision logic. They probably > need to be eta-converted (aka lambda-quoted) if they have any > side-effects. For more amusement on eta-conversion/lambda-quoting, > you might like this: > https://cr.openjdk.org/~jrose/jls/eta-abstraction-expr-quoting.html > -------------- next part -------------- An HTML attachment was scrubbed... URL: From gavin.bierman at oracle.com Mon Dec 18 10:59:43 2023 From: gavin.bierman at oracle.com (Gavin Bierman) Date: Mon, 18 Dec 2023 10:59:43 +0000 Subject: Change to JLS Chapter 5 Message-ID: <8AA29EB5-9772-4C94-98DA-08087BA46C44@oracle.com> [The following is only of interest to those concerned about very fine details of the JLS; no change to the language is proposed here.] Currently in the JLS we build the mechanics of pattern matching directly on top of casts. For example, in 14.30.2 (emphasis added): - A value v that is not the null reference matches a type pattern of type T if v **can be cast to** T without raising a ClassCastException; and does not match otherwise. This was useful as it made real the equivalence: if (e instanceof T t) { ? } if (e instanceof T) { T t = (T)e; ?} But, in terms of the JLS, there are some shortcomings. First, the semantics of pattern matching above should really refer to casting _conversions_ rather than casts. Oh well. But digging a little deeper there are further issues. Section 5.5 defines cast _contexts_ - the places where cast conversions can be applied. Except that it is all a little misleading. It currently says "Casting contexts allow the operand of a cast expression (15.16) to be converted to the type explicitly named by the cast operator.? but that?s not really true as we just agreed that we want to cover the new pattern matching contexts too. Sigh. It also gets complicated by the fact that we don?t want to use all the cast conversions either - we exclude any narrowing reference conversions that are unchecked AND we actually don?t use any of the cast conversions that deal with primitive types (these are all excluded by the definition of ?applicability? of patterns). We deal with this by introducing the concept of "checked cast convertible" in 5.5 and relying on the fact that we only consider applicable patterns. This is all okay, but with the changes proposed by JEP 455 (Primitive types in patterns, instanceof, and switch) coming real soon now, it is time to tidy up this treatment and set a proper foundation. To this end, we propose to add a new, seventh sort of context to the list given in the introduction to chapter 5 - called *testing contexts* - to handle pattern matching, and also define a notion of *test conversion*. As an example, the rule from the semantics of pattern matching given above, can now be expressed as: - A value v that is not the null reference matches a type pattern of type T if v can be converted by testing conversion to the target type T without raising a ClassCastException; and does not match otherwise. I have made a spec change document so you can see the details of this change that will appear in the Java SE 22 Edition of the spec: https://cr.openjdk.org/~gbierman/new-context/specs/new-context.html Again, I repeat, this is purely a presentational change at the level of the spec - the Java language itself is unchanged. Thanks, Gavin -------------- next part -------------- An HTML attachment was scrubbed... URL: From brian.goetz at oracle.com Tue Dec 19 00:34:57 2023 From: brian.goetz at oracle.com (Brian Goetz) Date: Mon, 18 Dec 2023 19:34:57 -0500 Subject: Effect cases in switch In-Reply-To: <1882982839.81146561.1702471762200.JavaMail.zimbra@univ-eiffel.fr> References: <327307323.80228032.1702420883408.JavaMail.zimbra@univ-eiffel.fr> <1882982839.81146561.1702471762200.JavaMail.zimbra@univ-eiffel.fr> Message-ID: <5e502a4a-7422-476a-9439-8f8a50303849@oracle.com> Turning to Remi's other comments: these are basically about interaction of this feature with the two throw-related features that came in in Java 7 -- multi-catch and try-with-resources. In Java 7 we got multi-catch; you can say ?? catch (E1|E2 e) { ... } There are several problems with multi-catch; most notably, the typing of the catch formal (it is a LUB, rather than an intersection type.)? This in turn causes another problem -- a multi-catch that does `throw e` should fail (since the LUB of two exceptions may well not be permitted by the throws clause of the enclosing method, even if both alternatives are.)? So another hack was added when adding multi-catch; that if the operand of `throw` is an effectively-final catch formal, it is allowed. Separately, some day we may want to make the operand of a catch clause a pattern, and not just a declaration of a local variable whose type extends Throwable.? This would be useful when we add deconstructors that support exception chaining: ??? catch (RuntimeException(IOException e)): ... But it is far from clear that we want (a) to support union type patterns or (b) if we did, we want the syntax to be `A|B e`.? We will surely have to navigate this when we get to the point where we consider making `catch` a pattern-aware context.? Presumably, once we address this for `catch`, the story for `case throws` will follow.? But it is not require that we mirror the behavior of `catch` in `case throws`, any more than we were forced to do constant patterns when we did patterns in switch (even though you can say `case 0`.)? So I think we do not have to solve this now, maybe we will address it later in conjunction with catch clauses, and maybe we never will, and all of those outcomes are OK. Turning to try-with-resources; there's an alternate form of the `try` header which can declare one or more resources: ??? try (R r = e) When this is present, one or more implicit finally clauses will ensure that the close() method is called. Does this mean than an exceptions-aware switch has to do the same?? I think the answer is a pretty clear "no".? In a switch: ??? switch (f(n)) { ... } where evaluation of f(n) might throw, we _already_ expect f to have cleaned up any resources it opened, before returning.? There is no need for switch to do more.? The reason this case is different is that the TWR header is opening a resource that the entirety of the try block needs to be able to operate on; in the case of a single-expression selector, it's a different story. So I think the answer to the first is "maybe, someday" and the second is "nope". -------------- next part -------------- An HTML attachment was scrubbed... URL: From angelos.bimpoudis at oracle.com Fri Dec 22 10:48:41 2023 From: angelos.bimpoudis at oracle.com (Angelos Bimpoudis) Date: Fri, 22 Dec 2023 10:48:41 +0000 Subject: Updated Draft Spec for Primitive types in patterns, instanceof, and switch (Preview) Message-ID: Dear experts, The draft of the spec covering JEP 455 (Primitive types in patterns, instanceof, and switch (Preview)) is updated at: https://cr.openjdk.org/~abimpoudis/instanceof/latest/ Even though we have discussed the spec before on the list (and the semantics have not changed), there are a few significant improvements in this updated draft: - The spec draft is rebased on top of JLS 22 (inheriting an editorial change that introduces testing contexts (5.7), and incorporating all the changes for unnamed patterns and variables from JEP 456). - It streamlines the definitions of exact conversions and unconditionally exact conversions. Feel free to send an email on this list for any comments. Many thanks, Aggelos -------------- next part -------------- An HTML attachment was scrubbed... URL: