Optimistic totality
Brian Goetz
brian.goetz at oracle.com
Mon Aug 24 16:23:47 UTC 2020
As I mentioned yesterday, I think our ideas about totality and null
handling were getting polluted by our desire to support intuitive,
optimistic totality. So let's try to separate them, by outlining some
goals for optimistic totality.
First, I'll posit that we're now able to stand on a more solid foundation:
- Null is just another value for purposes of pattern matching; total
patterns match null.
- Null is just another value for purposes of switches; switches will
feed null into total cases.
- The perceived null-hostility of switch is actually about switches on
enums, boxes, and strings; in the general case, we don't want, or need,
such null-hostility.
This is a much simpler story, and has many fewer sharp edges.
Declaring clarity on that, we now have two additional problems to solve:
- How to fill the gap between (always total) expression switches and
(always partial) statement switches. Totality checking is a useful
static analysis that can identify bugs earlier, and being able to
restore symmetry in semantics (even if it requires asymmetry in syntax)
reduces unexpected errors and potholes.
- How to extend the optimistic totality of expression switches over
enums (which is a very restricted case) to the more general case of
switches over sealed types, and switches with weakly total cases (such
as total deconstruction patterns.)
This mail will focus mostly on the second problem; I'll start another
thread for the first.
The goal of optimistic totality handling is to allow users to write a
set cases that covers the target "well enough" that a catch-all throwing
default is not needed. This has two benefits:
- Let the compiler write the dumb do-nothing code, rather than making
the user do it;
- If the user writes a throwing catch-all, we lose the opportunity to
type-check the assumption that the switch was total in the first place.
Users are well aware of the first benefit, but the second benefit is
actually more important. If the user writes:
Freq frequency = switch (trafficLight) {
case RED -> Freq.ofTHz(450);
case YELLOW -> Freq.ofTHz(525);
default -> throw ...;
}
We are deprived of two ways to help:
- We cannot tell whether the user meant for { RED, YELLOW } to cover
the space, so we cannot offer helpful type checking of "you forgot green."
- Even if the code, as written, does cover the space, if a new
constant / permitted subtype is added later, we lose the opportunity to
catch it at next compilation, and alert the user to the fact that their
assumption of totality was broken by someone else.
On the other hand, if there is no default clause, we get exhaustiveness
checking when the code is first written, and continual revalidation of
this assumption on every recompile.
OK, so optimistic totality is good. What does that really mean? We
already know one case:
- Specifying all the known constants of an enum, but no default or
null case.
Because this case is so limited, we handled this one pretty well in 12;
we NPE on null, and ICCE on everything else.
Another (new) case is:
- When we have a _weakly total_ (total except for null) pattern on the
target type. A key category of weakly total patterns are deconstruction
patterns whose sub-patterns are total. Such as:
var x = switch (box) {
case Box(var x) b -> ...
}
The pattern `Box(var x)` matches all _non-null_ boxes. (It can't match
null boxes, because we'd be invoking the deconstructor with a null
receiver, which would surely NPE anyway, since a deconstructor is going
to have some `x = this.x` ~99.99% of the time.) So, should this be good
enough to declare the switch optimistically total? I think so; having
to say `case null` in this switch would be irritating to users for no
good reason.
What we've done is flipped things around; rather than saying "switches
NPE on null", we can say "total switches with optimistically total case
sets can throw on silly inputs" -- because the very concept of
optimistic totality suggests that we think the residue consists only of
silly inputs (and we are only throwing when the switch is total
anyway.) Now we can have a more refined definition of silly inputs.
Another case:
- The sealed class analogue of an enum switch.
Here, we have a sealed class C, and a set of patterns that, for every
permitted subtype D of C, some subset of the patterns is
(optimistically) total on D. Now, our residue has two inhabitants:
null, and novel subclasses.
Do we think this should be optimistically total? Yes; all the reasons
why a throwing default is bad on the enum case apply to the sealed case,
there is just a larger residue set.
Another case:
- When we have a deconstructor D(C), and a set of patterns
D(P1)...D(Pn) such that P1..Pn are optimistically total on C, we would
like to conclude that the lifted patterns are optimistically total on D.
Example:
switch (boxOfShape) { // Shape = Circle + Rect
case Box(Circle c):
case Box(Rect r):
}
Our claim here is that because Circle + Rect are o.t. on Shape,
Box(Circle)+Box(Rect) should be o.t. on Box<Shape>. Do we buy that?
Again, I think we want this; asking users to insert Box(null) or
Box(novel) cases to get totality checking is counterproductive.
What we see here is that we have an accumulation of situations where we
think a given set of patterns covers a target type "well enough" that we
are willing to (a) let the user skate on being truly total, and (b)
engage enhanced type checking against the set of "good enough" cases.
After writing this, I think we are, once again, being overly constrained
by (and worse, distracted by) "consistency" with what we decided in 12
for the simple case of enum switches: that the answer always has to be
some form of ICCE or NPE. These were easy answers when the residue was
so easily characterized, but trying to extrapolate from them too rigidly
may be a mistake.
So, I think that we should save NPE and ICCE for the more accurate,
narrow uses we found for them in 12, and for any "complex" residue, just
define a new exception type -- and focus our energy on ensuring we get
good error messages out of it, and move past this distraction.
The real point here is defining what we consider to be acceptable
residue for an optimistically total switch, and ensure that we can
deliver clear error messages when a Box(Hexagon) shows up.
-------------- next part --------------
An HTML attachment was scrubbed...
URL: <https://mail.openjdk.java.net/pipermail/amber-spec-experts/attachments/20200824/0a8f9f43/attachment-0001.htm>
More information about the amber-spec-experts
mailing list