Re: [External] : Re: Remainder in pattern matching
Yes, this clears up my concerns. On Fri, Apr 1, 2022 at 6:56 AM Brian Goetz wrote: > > It seems pretty hard to land anywhere other than where you've landed, for > most of this. I have the same sort of question as Dan: do we really want to > wrap exceptions thrown by other patterns? You say we want to discourage > patterns from throwing at all, and that's a lovely dream, but the behavior > of total patterns is to throw when they meet something in their remainder. > > > Not exactly. The behavior of *switch* is to throw when they meet > something in the remainder of *all their patterns*. For example: > > Box> bbs = new Box(null); > switch (bbs) { > case Box(Box(String s)): ... > case null, Box b: ... > } > > has no remainder and will not throw. Box(null) doesn't match the first > pattern, because when we unroll to what amounts to > > if (x instanceof Box alpha && alpha != null && alpha.value() > instanceof Box beta && beta != null) { > s = beta.value(); ... > } > else if (x == null || x instanceof Box) { ... } > > we never dereference something we don't know to be non-null. So Box(null) > doesn't match the first case, but the second case gets a shot at it. Only > if no case matches does switch throw; *pattern matching* should never > throw. (Same story with let, except its like a switch with one > putatively-exhaustive case.) > > Since user-defined patterns will surely involve primitive patterns at some > point, there is the possibility that one of those primitive patterns > throws, which bubbles up as an exception thrown by a user-defined pattern. > > > Again, primitive patterns won't throw, they just won't match. Under the > rules I outlined last time, if I have: > > Box b = new Box(null); > switch (b) { > case Box(int x): ... > ... > } > > when we try to match Box(int x) to Box(null), it will not NPE, it will > just not match, and we'll go on to the next case. If all cases don't > match, then the switch will throw ME, which is a failure of > *exhaustiveness*, not a failure in *pattern matching*. > > Does this change your first statement? > > > On Wed, Mar 30, 2022 at 7:40 AM Brian Goetz > wrote: > >> We should have wrapped this up a while ago, so I apologize for the late >> notice, but we really have to wrap up exceptions thrown from pattern >> contexts (today, switch) when an exhaustive context encounters a >> remainder. I think there's really one one sane choice, and the only thing >> to discuss is the spelling, but let's go through it. >> >> In the beginning, nulls were special in switch. The first thing is to >> evaluate the switch operand; if it is null, switch threw NPE. (I don't >> think this was motivated by any overt null hostility, at least not at >> first; it came from unboxing, where we said "if its a box, unbox it", and >> the unboxing throws NPE, and the same treatment was later added to enums >> (though that came out in the same version) and strings.) >> >> We have since refined switch so that some switches accept null. But for >> those that don't, I see no other move besides "if the operand is null and >> there is no null handling case, throw NPE." Null will always be a special >> remainder value (when it appears in the remainder.) >> >> In Java 12, when we did switch expressions, we had to confront the issue >> of novel enum constants. We considered a number of alternatives, and came >> up with throwing ICCE. This was a reasonable choice, though as it turns >> out is not one that scales as well as we had hoped it would at the time. >> The choice here is based on "the view of classfiles at compile time and run >> time has shifted in an incompatible way." ICCE is, as Kevin pointed out, a >> reliable signal that your classpath is borked. >> >> We now have two precedents from which to extrapolate, but as it turns >> out, neither is really very good for the general remainder case. >> >> Recall that we have a definition of _exhaustiveness_, which is, at some >> level, deliberately not exhaustive. We know that there are edge cases for >> which it is counterproductive to insist that the user explicitly cover, >> often for two reasons: one is that its annoying to the user (writing cases >> for things they believe should never happen), and the other that it >> undermines type checking (the most common way to do this is a default >> clause, which can sweep other errors under the rug.) >> >> If we have an exhaustive set of patterns on a type, the set of possible >> values for that type that are not covered by some pattern in the set is >> called the _remainder_. Computing the remainder exactly is hard, but >> computing an upper bound on the remainder is pretty easy. I'll say "x may >> be in the remainder of P* on T" to indicate that we're defining the upper >> bound. >> >> - If P* contains a deconstruction pattern P(Q*), null may be in the >> remainder of P*. >> - If T is sealed, instances of a novel
Re: [External] : Re: Remainder in pattern matching
It seems pretty hard to land anywhere other than where you've landed, for most of this. I have the same sort of question as Dan: do we really want to wrap exceptions thrown by other patterns? You say we want to discourage patterns from throwing at all, and that's a lovely dream, but the behavior of total patterns is to throw when they meet something in their remainder. Not exactly. The behavior of *switch* is to throw when they meet something in the remainder of *all their patterns*. For example: Box> bbs = new Box(null); switch (bbs) { case Box(Box(String s)): ... case null, Box b: ... } has no remainder and will not throw. Box(null) doesn't match the first pattern, because when we unroll to what amounts to if (x instanceof Box alpha && alpha != null && alpha.value() instanceof Box beta && beta != null) { s = beta.value(); ... } else if (x == null || x instanceof Box) { ... } we never dereference something we don't know to be non-null. So Box(null) doesn't match the first case, but the second case gets a shot at it. Only if no case matches does switch throw; *pattern matching* should never throw. (Same story with let, except its like a switch with one putatively-exhaustive case.) Since user-defined patterns will surely involve primitive patterns at some point, there is the possibility that one of those primitive patterns throws, which bubbles up as an exception thrown by a user-defined pattern. Again, primitive patterns won't throw, they just won't match. Under the rules I outlined last time, if I have: Box b = new Box(null); switch (b) { case Box(int x): ... ... } when we try to match Box(int x) to Box(null), it will not NPE, it will just not match, and we'll go on to the next case. If all cases don't match, then the switch will throw ME, which is a failure of *exhaustiveness*, not a failure in *pattern matching*. Does this change your first statement? On Wed, Mar 30, 2022 at 7:40 AM Brian Goetz wrote: We should have wrapped this up a while ago, so I apologize for the late notice, but we really have to wrap up exceptions thrown from pattern contexts (today, switch) when an exhaustive context encounters a remainder. I think there's really one one sane choice, and the only thing to discuss is the spelling, but let's go through it. In the beginning, nulls were special in switch. The first thing is to evaluate the switch operand; if it is null, switch threw NPE. (I don't think this was motivated by any overt null hostility, at least not at first; it came from unboxing, where we said "if its a box, unbox it", and the unboxing throws NPE, and the same treatment was later added to enums (though that came out in the same version) and strings.) We have since refined switch so that some switches accept null. But for those that don't, I see no other move besides "if the operand is null and there is no null handling case, throw NPE." Null will always be a special remainder value (when it appears in the remainder.) In Java 12, when we did switch expressions, we had to confront the issue of novel enum constants. We considered a number of alternatives, and came up with throwing ICCE. This was a reasonable choice, though as it turns out is not one that scales as well as we had hoped it would at the time. The choice here is based on "the view of classfiles at compile time and run time has shifted in an incompatible way." ICCE is, as Kevin pointed out, a reliable signal that your classpath is borked. We now have two precedents from which to extrapolate, but as it turns out, neither is really very good for the general remainder case. Recall that we have a definition of _exhaustiveness_, which is, at some level, deliberately not exhaustive. We know that there are edge cases for which it is counterproductive to insist that the user explicitly cover, often for two reasons: one is that its annoying to the user (writing cases for things they believe should never happen), and the other that it undermines type checking (the most common way to do this is a default clause, which can sweep other errors under the rug.) If we have an exhaustive set of patterns on a type, the set of possible values for that type that are not covered by some pattern in the set is called the _remainder_. Computing the remainder exactly is hard, but computing an upper bound on the remainder is pretty easy. I'll say "x may be in the remainder of P* on T" to indicate that we're defining the upper bound. - If P* contains a deconstruction pattern P(Q*), null may be in the remainder of P*. - If T is sealed, instances of a novel subtype of T may be in the remainder of P*. - If T is an enum, novel enum constants of T may be
Re: [External] : Re: Remainder in pattern matching
Here's some candidate spec text for MatchException: Prototype spec for MatchException ( a preview API class ). Thrown to indicate an unexpected failure in pattern matching. MatchException may be thrown when an exhaustive pattern matching language construct (such as a switch expression) encounters a match target that does not match any of the provided patterns at runtime. This can arise from a number of cases: - Separate compilation anomalies, where a sealed interface has a different set of permitted subtypes at runtime than it had at compilation time, an enum has a different set of constants at runtime than it had at compilation time, or the type hierarchy has changed in incompatible ways between compile time and run time. - Null targets and sealed types. If an interface or abstract class `C` is sealed to permit `A` and `B`, then the set of record patterns `R(A a)` and `R(B b)` are exhaustive on a record `R` whose sole component is of type `C`, but neither of these patterns will match `new R(null)`. - Null targets and nested record patterns. Given a record type `R` whose sole component is `S`, which in turn is a record whose sole component is `String`, then the nested record pattern `R(S(String s))` will not match `new R(null)`. Match failures arising from unexpected inputs will generally throw `MatchException` only after all patterns have been tried; even if `R(S(String s))` does not match `new R(null)`, a later pattern (such as `R r`) may still match the target. MatchException may also be thrown when operations performed as part of pattern matching throw an unexpected exception. For example, pattern matching may cause methods such as record component accessors to be implicitly invoked in order to extract pattern bindings. If these methods throw an exception, execution of the pattern matching construct may fail with `MatchException`. On 3/30/2022 2:43 PM, Dan Heidinga wrote: On Wed, Mar 30, 2022 at 2:38 PM Brian Goetz wrote: Another way to think about this is: - If any of the code that the user actually wrote (the RHS of case clauses, or guards on case labels) throws, then the switch throws that - If any of the machinery of the switch dispatch throws, it throws MatchException. That's a reasonable way to factor this and makes the difference between the machinery and the direct user code clear, even when looking at stacktraces. And from your other response: Another thing it gains is that it discourages people from thinking they can use exceptions in dtors; having these laundered through MatchException discourages using this as a side channel, though that's a more minor thing. This is a stronger argument than you give it credit for being. Wrapping the exception adds a bit of friction to doing the wrong thing which will pay off in helping guide users to the intended behaviour. --Dan On 3/30/2022 2:12 PM, Dan Heidinga wrote: The rules regarding NPE, ICCE and MatchException look reasonable to me. As a separate but not-separate exception problem, we have to deal with at least two additional sources of exceptions: - A dtor / record acessor may throw an arbitrary exception in the course of evaluating whether a case matches. - User code in the switch may throw an arbitrary exception. For the latter, this has always been handled by having the switch terminate abruptly with the same exception, and we should continue to do this. For the former, we surely do not want to swallow this exception (such an exception indicates a bug). The choices here are to treat this the same way we do with user code, throwing it out of the switch, or to wrap with MatchException. I prefer the latter -- wrapping with MatchException -- because the exception is thrown from synthetic code between the user code and the ultimate thrower, which means the pattern matching feature is mediating access to the thrower. I think we should handle this as "if a pattern invoked from pattern matching completes abruptly by throwing X, pattern matching completes abruptly with MatchException", because the specific X is not a detail we want the user to bind to. (We don't want them to bind to anything, but if they do, we want them to bind to the logical action, not the implementation details.) My intuition (and maybe I have the wrong mental model?) is that the pattern matching calling a user written dtor / record accessor is akin to calling a method. We don't wrap the exceptions thrown by methods apart from some very narrow cases (ie: reflection), and I thought part of reflection's behaviour was related to needing to ensure exceptions (particularly checked ones) were converted to something explicitly handled by the caller. If the dtor / record accessor can declare they throw checked exceptions, then I can kind of see the rationale for wrapping them. Otherwise, it seems clearer to me to let them be thrown without wrapping. I don't think we expect users to explicitly
Re: [External] : Re: Remainder in pattern matching
- Original Message - > From: "Dan Heidinga" > To: "Brian Goetz" > Cc: "amber-spec-experts" > Sent: Wednesday, March 30, 2022 8:43:42 PM > Subject: Re: [External] : Re: Remainder in pattern matching > On Wed, Mar 30, 2022 at 2:38 PM Brian Goetz wrote: >> [...] > > And from your other response: > >> Another thing it gains is that it discourages people >> from thinking they can use exceptions in dtors; having these laundered >> through MatchException discourages using this as a side channel, though >> that's a more minor thing. > > This is a stronger argument than you give it credit for being. > Wrapping the exception adds a bit of friction to doing the wrong thing > which will pay off in helping guide users to the intended behaviour. Wrapping exceptions into a MatchException seems a very bad idea to me. When you compute something on an AST, the pattern matching is recursive, so if an exception occurs, instead of having one exception with a long stacktrace, we will get a linked list of MatchException with each of them having a long stacktraces. > > --Dan Rémi > >> On 3/30/2022 2:12 PM, Dan Heidinga wrote: >> >> The rules regarding NPE, ICCE and MatchException look reasonable to me. >> >> >> As a separate but not-separate exception problem, we have to deal with at >> least >> two additional sources of exceptions: >> >> - A dtor / record acessor may throw an arbitrary exception in the course of >> evaluating whether a case matches. >> >> - User code in the switch may throw an arbitrary exception. >> >> For the latter, this has always been handled by having the switch terminate >> abruptly with the same exception, and we should continue to do this. >> >> For the former, we surely do not want to swallow this exception (such an >> exception indicates a bug). The choices here are to treat this the same way >> we >> do with user code, throwing it out of the switch, or to wrap with >> MatchException. >> >> I prefer the latter -- wrapping with MatchException -- because the exception >> is >> thrown from synthetic code between the user code and the ultimate thrower, >> which means the pattern matching feature is mediating access to the thrower. >> I >> think we should handle this as "if a pattern invoked from pattern matching >> completes abruptly by throwing X, pattern matching completes abruptly with >> MatchException", because the specific X is not a detail we want the user to >> bind to. (We don't want them to bind to anything, but if they do, we want >> them >> to bind to the logical action, not the implementation details.) >> >> My intuition (and maybe I have the wrong mental model?) is that the >> pattern matching calling a user written dtor / record accessor is akin >> to calling a method. We don't wrap the exceptions thrown by methods >> apart from some very narrow cases (ie: reflection), and I thought part >> of reflection's behaviour was related to needing to ensure exceptions >> (particularly checked ones) were converted to something explicitly >> handled by the caller. >> >> If the dtor / record accessor can declare they throw checked >> exceptions, then I can kind of see the rationale for wrapping them. >> Otherwise, it seems clearer to me to let them be thrown without >> wrapping. >> >> I don't think we expect users to explicitly handle MatchException when >> using pattern matching so what does wrapping gain us here? >> >> --Dan >>
Re: [External] : Re: Remainder in pattern matching
Yes, and this is a special case of a more general thing -- that while pattern declarations may have a lot in common with methods, they are not "just methods with multiple return" (e.g., they have a different set of characteristics at the declaration, they are intrinsically conditional, they are "invoked" differently.) While their bodies may look method-like, and ultimately they boil down to methods, thinking "they are just methods" is likely to drag you to the wrong place. Of course, its a balance between how similar and HOW DIFFERENT they are, and that's what we're looking for. Another thing it gains is that it discourages people from thinking they can use exceptions in dtors; having these laundered through MatchException discourages using this as a side channel, though that's a more minor thing. This is a stronger argument than you give it credit for being. Wrapping the exception adds a bit of friction to doing the wrong thing which will pay off in helping guide users to the intended behaviour.
Re: [External] : Re: Remainder in pattern matching
> From: "Brian Goetz" > To: "Remi Forax" > Cc: "amber-spec-experts" > Sent: Wednesday, March 30, 2022 8:35:17 PM > Subject: Re: [External] : Re: Remainder in pattern matching >> It seems that what you are saying is that you think an Exception is better >> than >> an Error. > Not exactly; what I'm saying is that the attempt to separate stray nulls from > separate compilation issues here seems like a heroic effort for low value, and > I'd rather have one channel for "exhaustiveness failure" and let > implementations decide how heroic they want to get in sorting out the possible > causes. NPE is a developer issue, separate compilation failure/ICCE is a deployment issue, there is no point to have one channel. Rémi
Re: [External] : Re: Remainder in pattern matching
On Wed, Mar 30, 2022 at 2:38 PM Brian Goetz wrote: > > Another way to think about this is: > > - If any of the code that the user actually wrote (the RHS of case clauses, > or guards on case labels) throws, then the switch throws that > - If any of the machinery of the switch dispatch throws, it throws > MatchException. > That's a reasonable way to factor this and makes the difference between the machinery and the direct user code clear, even when looking at stacktraces. And from your other response: > Another thing it gains is that it discourages people > from thinking they can use exceptions in dtors; having these laundered > through MatchException discourages using this as a side channel, though > that's a more minor thing. This is a stronger argument than you give it credit for being. Wrapping the exception adds a bit of friction to doing the wrong thing which will pay off in helping guide users to the intended behaviour. --Dan > On 3/30/2022 2:12 PM, Dan Heidinga wrote: > > The rules regarding NPE, ICCE and MatchException look reasonable to me. > > > As a separate but not-separate exception problem, we have to deal with at > least two additional sources of exceptions: > > - A dtor / record acessor may throw an arbitrary exception in the course of > evaluating whether a case matches. > > - User code in the switch may throw an arbitrary exception. > > For the latter, this has always been handled by having the switch terminate > abruptly with the same exception, and we should continue to do this. > > For the former, we surely do not want to swallow this exception (such an > exception indicates a bug). The choices here are to treat this the same way > we do with user code, throwing it out of the switch, or to wrap with > MatchException. > > I prefer the latter -- wrapping with MatchException -- because the exception > is thrown from synthetic code between the user code and the ultimate thrower, > which means the pattern matching feature is mediating access to the thrower. > I think we should handle this as "if a pattern invoked from pattern matching > completes abruptly by throwing X, pattern matching completes abruptly with > MatchException", because the specific X is not a detail we want the user to > bind to. (We don't want them to bind to anything, but if they do, we want > them to bind to the logical action, not the implementation details.) > > My intuition (and maybe I have the wrong mental model?) is that the > pattern matching calling a user written dtor / record accessor is akin > to calling a method. We don't wrap the exceptions thrown by methods > apart from some very narrow cases (ie: reflection), and I thought part > of reflection's behaviour was related to needing to ensure exceptions > (particularly checked ones) were converted to something explicitly > handled by the caller. > > If the dtor / record accessor can declare they throw checked > exceptions, then I can kind of see the rationale for wrapping them. > Otherwise, it seems clearer to me to let them be thrown without > wrapping. > > I don't think we expect users to explicitly handle MatchException when > using pattern matching so what does wrapping gain us here? > > --Dan > >
Re: [External] : Re: Remainder in pattern matching
- Original Message - > From: "Brian Goetz" > To: "Dan Heidinga" > Cc: "amber-spec-experts" > Sent: Wednesday, March 30, 2022 8:26:53 PM > Subject: Re: [External] : Re: Remainder in pattern matching [...] > > One thing wrapping gains is that it gives us a place to centralize > "something failed in pattern matching", which includes exhaustiveness > failures as well as failures of invariants which PM assumes (e.g., dtors > don't throw.) but such centralization is a bad practice, that the reason why catch(Exception) is considered as a bad practice. BTW, i hope that with loom people will use virtual threads (they are cheap) to manage scenarios where you want to discard a computation if something fails, like in Erlang. Rémi
Re: [External] : Re: Remainder in pattern matching
Another way to think about this is: - If any of the code that the user actually wrote (the RHS of case clauses, or guards on case labels) throws, then the switch throws that - If any of the machinery of the switch dispatch throws, it throws MatchException. On 3/30/2022 2:12 PM, Dan Heidinga wrote: The rules regarding NPE, ICCE and MatchException look reasonable to me. As a separate but not-separate exception problem, we have to deal with at least two additional sources of exceptions: - A dtor / record acessor may throw an arbitrary exception in the course of evaluating whether a case matches. - User code in the switch may throw an arbitrary exception. For the latter, this has always been handled by having the switch terminate abruptly with the same exception, and we should continue to do this. For the former, we surely do not want to swallow this exception (such an exception indicates a bug). The choices here are to treat this the same way we do with user code, throwing it out of the switch, or to wrap with MatchException. I prefer the latter -- wrapping with MatchException -- because the exception is thrown from synthetic code between the user code and the ultimate thrower, which means the pattern matching feature is mediating access to the thrower. I think we should handle this as "if a pattern invoked from pattern matching completes abruptly by throwing X, pattern matching completes abruptly with MatchException", because the specific X is not a detail we want the user to bind to. (We don't want them to bind to anything, but if they do, we want them to bind to the logical action, not the implementation details.) My intuition (and maybe I have the wrong mental model?) is that the pattern matching calling a user written dtor / record accessor is akin to calling a method. We don't wrap the exceptions thrown by methods apart from some very narrow cases (ie: reflection), and I thought part of reflection's behaviour was related to needing to ensure exceptions (particularly checked ones) were converted to something explicitly handled by the caller. If the dtor / record accessor can declare they throw checked exceptions, then I can kind of see the rationale for wrapping them. Otherwise, it seems clearer to me to let them be thrown without wrapping. I don't think we expect users to explicitly handle MatchException when using pattern matching so what does wrapping gain us here? --Dan
Re: [External] : Re: Remainder in pattern matching
It seems that what you are saying is that you think an Exception is better than an Error. Not exactly; what I'm saying is that the attempt to separate stray nulls from separate compilation issues here seems like a heroic effort for low value, and I'd rather have one channel for "exhaustiveness failure" and let implementations decide how heroic they want to get in sorting out the possible causes.
Re: [External] : Re: Remainder in pattern matching
> From: "Brian Goetz" > To: "Remi Forax" > Cc: "amber-spec-experts" > Sent: Wednesday, March 30, 2022 6:32:15 PM > Subject: Re: [External] : Re: Remainder in pattern matching >> For when the static world and the dynamic world disagree, i think your >> analysis >> has miss an important question, switching on an enum throw an ICCE very late >> when we discover an unknown value, but in the case of a sealed type, > Actually, I thought about that quite a bit before proposing this. And my > conclusion is: using ICCE was mostly a (well intentioned) mistake here, and > "doubling down" on that path is more trouble than it is worth. So we are > minimally consistent with the ICCE choice in the cases that were compilable in > 12, but for anything else, we follow the general rule. > The thought experiment that I did was: what if we had not done switch > expressions in 12. Then the only precedent we have to deal with is the null > case, which has a pretty obvious answer. So what would we do? Would we > introduce 10s of catch-all cases solely for the purpose of diagnosing the > source of remainder, or would we introduce a throwing default that throws > MatchException on everything but null? I concluded we would do the latter, so > what is proposed here is basically that, but carving out the 12-compatibility > case. We are discussing about what to do if a sealed types has more permitted subtypes at runtime than the one seen when the code was compiled. It's a separate compilation issue, hence the ICCE. It seems that what you are saying is that you think an Exception is better than an Error. If we follow that path, it means that it may make sense to recover from a MatchException but i fail to see how, we can not ask a developer of a code to change it while that code is executed, separate compilation errors are not recoverable. >> Remainders are dangling else in a cascade of if ... else, so yes, we have to >> care of them. > Yes, but we can care for all of them in one swoop with a synthetic default. >> So yes, it may a lot of bytecodes if we choose to add all branches but the >> benefit is not questionable, it's far better than the alternative which is >> GoodLuckFigureByYourselfException. > Yes, when you get a dynamic error here in a complex switch, the range of what > could have gone wrong is large. (The same will be true outside of switches > when > we have more kinds of patterns (list patterns, map patterns, etc) and more > ways > to compose patterns into bigger patterns; if we have a big complex pattern > that > matches the JSON document with the keys we want, if it doesn't match because > (say) some integer nested nine levels deep overflowed 32 bits, this is also > going to be hard to diagnose.) But you are proposing a new and significant > language requirement -- that the language should mandate an arbitrarily > complex > explanation of why something didn't match. I won't dispute that this has > benefit -- but I am not convinced this is necessarily the place for this, or > whether the cost is justified by the benefit. The explanation is not complex, there is a sealed type has more subtypes now than at a time the code was compiled. Rémi
Re: [External] : Re: Remainder in pattern matching
It's a little like calling a method, but a little not like it too. For example, when you match on a record pattern: case Point(var x, var y): ... what may happen is *either* you will invoke a user-written deconstructor pattern, *or* we will test if you are a Point with `instanceof`, and then invoke the accessor methods (which might be user-written or implicit.) Similarly, if you match: case Point(P, Q): case Point(R, S): we may invoke the Point deconstructor once, or twice. And there's no way to _directly_ invoke a pattern, only through switch, instanceof, and other contexts. All of this means that invocations of pattern methods is more indirect, and mediated by the language, than invoking a method. When you invoke a method, you are assenting to its contract about what it returns, what it throws, etc. When you match a pattern, it feels more likely are assenting to the contract of _pattern matching_, which in turn hides implementation details of what pattern methods are invoked, when they are invoked, how often, etc. Dtors and record accessors cannot throw checked exceptions at all, and will be discouraged from throwing exceptions at all. One thing wrapping gains is that it gives us a place to centralize "something failed in pattern matching", which includes exhaustiveness failures as well as failures of invariants which PM assumes (e.g., dtors don't throw.) Another thing it gains is that it discourages people from thinking they can use exceptions in dtors; having these laundered through MatchException discourages using this as a side channel, though that's a more minor thing. Agree we do not expect users to explicitly handle ME, any more so than NPE. My intuition (and maybe I have the wrong mental model?) is that the pattern matching calling a user written dtor / record accessor is akin to calling a method. We don't wrap the exceptions thrown by methods apart from some very narrow cases (ie: reflection), and I thought part of reflection's behaviour was related to needing to ensure exceptions (particularly checked ones) were converted to something explicitly handled by the caller. If the dtor / record accessor can declare they throw checked exceptions, then I can kind of see the rationale for wrapping them. Otherwise, it seems clearer to me to let them be thrown without wrapping. I don't think we expect users to explicitly handle MatchException when using pattern matching so what does wrapping gain us here? --Dan
Re: [External] : Re: Remainder in pattern matching
For when the static world and the dynamic world disagree, i think your analysis has miss an important question, switching on an enum throw an ICCE very late when we discover an unknown value, but in the case of a sealed type, Actually, I thought about that quite a bit before proposing this. And my conclusion is: using ICCE was mostly a (well intentioned) mistake here, and "doubling down" on that path is more trouble than it is worth. So we are minimally consistent with the ICCE choice in the cases that were compilable in 12, but for anything else, we follow the general rule. The thought experiment that I did was: what if we had not done switch expressions in 12. Then the only precedent we have to deal with is the null case, which has a pretty obvious answer. So what would we do? Would we introduce 10s of catch-all cases solely for the purpose of diagnosing the source of remainder, or would we introduce a throwing default that throws MatchException on everything but null? I concluded we would do the latter, so what is proposed here is basically that, but carving out the 12-compatibility case. Remainders are dangling else in a cascade of if ... else, so yes, we have to care of them. Yes, but we can care for all of them in one swoop with a synthetic default. So yes, it may a lot of bytecodes if we choose to add all branches but the benefit is not questionable, it's far better than the alternative which is GoodLuckFigureByYourselfException. Yes, when you get a dynamic error here in a complex switch, the range of what could have gone wrong is large. (The same will be true outside of switches when we have more kinds of patterns (list patterns, map patterns, etc) and more ways to compose patterns into bigger patterns; if we have a big complex pattern that matches the JSON document with the keys we want, if it doesn't match because (say) some integer nested nine levels deep overflowed 32 bits, this is also going to be hard to diagnose.) But you are proposing a new and significant language requirement -- that the language should mandate an arbitrarily complex explanation of why something didn't match. I won't dispute that this has benefit -- but I am not convinced this is necessarily the place for this, or whether the cost is justified by the benefit. Also, note that the two are not inconsistent. If the switch is required to throw MatchException on remainder, the compiler is *allowed* to try and diagnose the root cause (the ME can wrap something more specific), but not required to. Pattern failure diagnosis then becomes a quality of implementation choice, rather than having complex, brittle rules mandated by the spec. There's nothing to stop us from doing the equivalent of the "helpful NPE" JEP in the future.