Record patterns (and beyond): exceptions
Remi Forax
forax at univ-mlv.fr
Thu Feb 17 10:45:18 UTC 2022
> From: "Brian Goetz" <brian.goetz at oracle.com>
> To: "amber-spec-experts" <amber-spec-experts at 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
-------------- next part --------------
An HTML attachment was scrubbed...
URL: <https://mail.openjdk.java.net/pipermail/amber-spec-experts/attachments/20220217/af6350a4/attachment.htm>
More information about the amber-spec-experts
mailing list