Record patterns (and beyond): exceptions
Brian Goetz
brian.goetz at oracle.com
Wed Feb 16 18:34:17 UTC 2022
As we move towards the next deliverable -- record patterns -- we have
two new questions regarding exceptions to answer.
#### Questions
1. When a dtor throws an exception. (You might think we can kick this
down the road, since records automatically acquire a synthetic dtor, and
users can't write dtors yet, but since that synthetic dtor will invoke
record component accessors, and users can override record component
accessors and therefore they can throw, we actually need to deal with
this now.)
This has two sub-questions:
1a. Do we want to do any extra type checking on the bodies of dtors /
record accessors to discourage explicitly throwing exceptions?
Obviously we cannot prevent exceptions like NPEs arising out of
dereference, but we could warn on an explicit throw statement in a
record accessor / dtor declaration, to remind users that throwing from
dtors is not the droid they are looking for.
1b. When the dtor for Foo in the switch statement below throws E:
switch (x) {
case Box(Foo(var a)): ...
case Box(Bar(var b)): ...
}
what should happen? Candidates include:
- allow the switch to complete abruptly by throwing E?
- same, but wrap E in some sort of ExceptionInMatcherException?
- ignore the exception and treat the match as having failed, and move
on to the next case?
2. Exceptions for remainder. We've established that there is a
difference between an _exhaustive_ set of patterns (one good enough to
satisfy the compiler that the switch is complete enough) and a _total_
set of patterns (one that actually covers all input values.) The
difference is called the _remainder_. For constructs that require
totality, such as pattern switches and let/bind, we have invariants
about what will have happened if the construct completes normally; for
switches, this means exactly one of the cases was selected. The
compiler must make up the difference by inserting a throwing catch-all,
as we already do with expression switches over enums, and all switches
over sealed types, that lack total/default clauses.
So far, remainder is restricted to two kinds of values: null (about
which switch already has a strong opinion) and "novel" enum constants /
novel subtypes of sealed types. For the former, we throw NPE; for the
latter, we throw ICCE.
As we look ahead to record patterns, there is a new kind of remainder:
the "spine" of nested record patterns. This includes things like
Box(null), Box(novel), Box(Bag(null)), Box(Mapping(null, novel)), etc.
It should be clear that there is no clean extrapolation from what we
currently do, to what we should do in these cases. But that's OK; both
of the existing remainder-rejection cases derive from "what does the
context think" -- switch hates null (so, NPE), and enum switches are a
thing (which makes ICCE on an enum switch reasonable.) But in the
general case, we'll want some sort of MatchRemainderException.
Note that throwing an exception from remainder is delayed until the last
possible moment. We could have:
case Box(Bag(var x)): ...
case Box(var x) when x == null: ...
and the reasonable treatment is to treat Box(Bag(var x)) as not matching
Box(null), even though it is exhuastive on Box<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.
#### Discussion
For (1a), my inclination is to do nothing for record accessors, but when
we get to explicit dtors, warning on explicit throw is not a bad idea.
Unlike ctors, where exceptions are part of the standard toolbox,
deconstructors are handed an already-constructed object, and are
supposed to be total. If you're inclined to write a partial dtor,
you're probably doing it wrong. As it is a new construct, the
additional error checking to guide people to its proper use is probably
reasonable (and cheap.)
For (1b), since an exception in a dtor is suppose to indicate an
exceptional failure, I don't think swallowing it and trying to go on
with the show is a good move. My preference would be to wrap the
exception, as we do with ExceptionInInitializerError, to make it clear
that an exception from a dtor is a truly unexpected thing, and clearly
name-and-shame the offending dtor. So there's a bikeshed to paint for
what we call this exception.
For (2), trying to repurpose either NPE or ICCE here is a losing move.
Better to invent an exception type that means "uncovered remainder"
(which is more akin to an IAE than anything else; someone passed a bad
value to an exhaustive-enough switch.) We would use the same exception
in let/bind, so this shouldn't have "switch" in its name, but probably
something more like MatchRemainderException.
-------------- next part --------------
An HTML attachment was scrubbed...
URL: <https://mail.openjdk.java.net/pipermail/amber-spec-experts/attachments/20220216/9f89bae8/attachment-0001.htm>
More information about the amber-spec-experts
mailing list