> De: "Brian Goetz" <brian.go...@oracle.com>
> À: "amber-spec-experts" <amber-spec-experts@openjdk.java.net>
> Envoyé: Lundi 24 Août 2020 18:23:47
> Objet: Optimistic totality

> 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.
yes, this point is very important, 
that one issue I currently have with the IDEs, when you write an expression 
switch on an enum, they tend to ask for a default even if the switch is 
exhaustive, but this has the side effect of not reporting an error if someone 
add a new case to the enum. 

> 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.
yes !, it's exactly what i've just said above ! 

> 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?
hum, this is where I don't like the current state of the syntax, because i 
don't know if this case is an instanceof or not, you seem to think it's not an 
instanceof and just a deconstruction, so the desconstructor can NPE because 
there is no instanceof upward. 
Forcing the user to use 'default' here makes the syntax clearer. 

With 
var x = switch(box) { 
default Box(var x) b -> 
}; 

This is now clear that this is not an instanceof, it's an "else", thus 
switch(null) can NPE. 

> I think so; having to say `case null` in this switch would be irritating to
> users for no good reason.
I agree 

> 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.)
yes 

> 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.
yes, 
NPE if null, ICCE if it's a novel type. 

> 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.
yes ! 

> 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.
I disagree, this argument seems weak, because it's not an argument for a new 
exception, it's a kind of argument for not using NPE and ICCE because a switch 
on type doesn't behave like a switch on enums. 
Also I still think we can bridge the gap with the switch on enums to make it 
works like a switch on types, in that case, your argument is moot. 

> 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.
yes, a good error message is more important than a new kind of exception. 
One minor issue I see is that the error message will contains a Pattern which 
is not among the patterns that are defined in the switch. 
For a ICCE, the pattern reported is Box(Hexagon) which is fine. 
For an NPE , by example in the case of the box of shapes, the pattern reported 
can be "Box(null)" but may be a Box(var) better suits the problem. 

Rémi 

Reply via email to