> From: "Brian Goetz" <brian.go...@oracle.com> > To: "amber-spec-experts" <amber-spec-experts@openjdk.java.net> > Sent: Wednesday, February 16, 2022 7:34:17 PM > Subject: Record patterns (and beyond): exceptions
> As we move towards the next deliverable -- record patterns -- we have two new > questions regarding exceptions to answer. > #### Questions > 1. When a dtor throws an exception. ( You might think we can kick this down > the > road, since records automatically acquire a synthetic dtor, and users can't > write dtors yet, but since that synthetic dtor will invoke record component > accessors, and users can override record component accessors and therefore > they > can throw, we actually need to deal with this now.) > This has two sub-questions: > 1a. Do we want to do any extra type checking on the bodies of dtors / record > accessors to discourage explicitly throwing exceptions? Obviously we cannot > prevent exceptions like NPEs arising out of dereference, but we could warn on > an explicit throw statement in a record accessor / dtor declaration, to remind > users that throwing from dtors is not the droid they are looking for. For de-constructor, given that they does not exist yet, we can do like record constructor, banned checked exceptions and for accessors, we can emit a warning as you suggest and do not allow record pattern if one of the getters throws a checked exception. > 1b. When the dtor for Foo in the switch statement below throws E: > switch (x) { > case Box(Foo(var a)): ... > case Box(Bar(var b)): ... > } > what should happen? Candidates include: > - allow the switch to complete abruptly by throwing E? > - same, but wrap E in some sort of ExceptionInMatcherException? > - ignore the exception and treat the match as having failed, and move on to > the > next case? The nice thing about the rules above is that a record pattern can never throw a checked exception. So there is nothing to do here. > 2. Exceptions for remainder. We've established that there is a difference > between an _exhaustive_ set of patterns (one good enough to satisfy the > compiler that the switch is complete enough) and a _total_ set of patterns > (one > that actually covers all input values.) The difference is called the > _remainder_. For constructs that require totality, such as pattern switches > and > let/bind, we have invariants about what will have happened if the construct > completes normally; for switches, this means exactly one of the cases was > selected. The compiler must make up the difference by inserting a throwing > catch-all, as we already do with expression switches over enums, and all > switches over sealed types, that lack total/default clauses. > So far, remainder is restricted to two kinds of values: null (about which > switch > already has a strong opinion) and "novel" enum constants / novel subtypes of > sealed types. For the former, we throw NPE; for the latter, we throw ICCE. > As we look ahead to record patterns, there is a new kind of remainder: the > "spine" of nested record patterns. This includes things like Box(null), > Box(novel), Box(Bag(null)), Box(Mapping(null, novel)), etc. It should be clear > that there is no clean extrapolation from what we currently do, to what we > should do in these cases. But that's OK; both of the existing > remainder-rejection cases derive from "what does the context think" -- switch > hates null (so, NPE), and enum switches are a thing (which makes ICCE on an > enum switch reasonable.) But in the general case, we'll want some sort of > MatchRemainderException. Nope, it can not be a runtime exception because people will write code to catch it and we will have a boat load of subtle bugs because exception are side effect so you can see in which order the de-constructors or the pattern methods are called. ICCE is fine. > Note that throwing an exception from remainder is delayed until the last > possible moment. We could have: > case Box(Bag(var x)): ... > case Box(var x) when x == null: ... > and the reasonable treatment is to treat Box(Bag(var x)) as not matching > Box(null), even though it is exhuastive on Box<Bag<?>>), and therefore fall > into the second case on Box(null). Only when we reach the end of the switch, > and we haven't matched any cases, do we throw MatchRemainderException. I really dislike that idea, it will be a burden in the future each time we want to change the implementation. I would like the semantics to make no promise about when the error will be thrown, the semantics should not be defined if a deconstructors/pattern method does a side effect, the same way the stream semantics is not defined if the lambda taken as parameter of Stream.map() does a side effect. I think the parallel between the pattern matching and a stream in term of execution semantics is important here. From the outside, those things are monads, they should work the same way. Rémi