Yes, this is the sort of ordering I was aiming at.

If the user does not want such implicit handling of an optimistically total 
situation in a statement switch, then it is always possible to provide explicit 
clauses “case null: break;” and “default: break;”.

Indeed, and this is why I was trying to break it down into a set of cases, to ensure that there always is a pattern the user can denote if they want to catch some part of the residue.  Where we are now is:

 - In a total switch (currently just switch expressions), any residue involving novel values gets ICCE, a null gets NPE, and any residue not in the above categories gets (something, maybe NPE, maybe something else.)

 - If the user explicitly wants Box(null), they have two choices: explicitly match Box(null), or, more likely, use some total pattern on Box (`Box(var x)`, `Box b`, etc.)  Similarly, if they want (for whatever reason) Box(novel), they can similarly use totality.  (I hope people are beginning to see why totality in nesting is so critical.)


So, next sub-subject (sub-ject?): when, and under what conditions, do we get NPE from non-total switches?  I said this yesterday:

Separately (but not really separately), I'd like to refine my claim that `switch` is null-hostile.  In reality, `switch` NPEs on null in three cases: a null enum, String, or primitive box.  And, in each of these cases, it NPEs because (the implementation) really does dereference the target!  For a `String`, it calls `hashCode()`.  For an `enum`, it calls `ordinal()`.  And for a box, it calls `xxxValue()`.  It is _those_ methods that NPE, not the switch.  (Yes, we could have designed it so that the implementation did a null check before calling those things.)

I bring this up because these situations cause current switch to NPE even when the switch is not total, and this muddies the story a lot.  We can refine this behavior by saying: "If a switch *on enums, strings, or boxes* has no nullable cases, then there is an implicit `case null: NPE` at the beginning".

In other words, I am proposing to treat this "preemptive throwing" as an artifact of switching over these special types (which is fair because the language already gives these types special treatment.)  Then, we are free to treat residue-handling as a consequence of totality, not a general null-hostility of switch.


Let me repeat that, because it's a big deal.

    Switch is *not* null-hostile.  We were just extrapolating from too few data points to
    see it.

    Switches on _enums, strings, and boxes_, that do not explicitly have null-handling cases,     are null-hostile, because switching on these involves calling methods on Enum, String,
    or {Integer,Long,...}.

    If you put a `case null` in a switch on strings/etc, it doesn't throw, it's just matching
    a value.

    In all other cases, null is just a value that can be matched, or not, and if the     switch ignores its residue, the nulls leak out just like the rest of it.

    In the general case, switches throw only when they are total; for partial switches     (e.g. statement switches), null is just another value that didn't get matched.

I believe this restores us to sanity.


Next up (separate topic): letting statement switches opt into totality.






Assuming that we're on the right track, and drilling into the next level, we now have to bring this back to totality.

On 8/20/2020 9:02 PM, Guy Steele wrote:

On Aug 20, 2020, at 6:14 PM, Brian Goetz <brian.go...@oracle.com> wrote:

I suspect there are other orderings too, such as "any nulls beat any novels" or 
vice versa, which would also be deterministic and potentially more natural to the user.  
But before we go there, I want to make sure we have something where users can understand 
the exceptions that are thrown without too much head-scratching.

If a user had:

     case Box(Head)
     case Box(Tail)

and a Box(null) arrived unexpectedly at the switch, would NPE really be what they expect? 
 An NPE happens when you _dereference_ a null.  But no one is deferencing anything here; 
it's just that Box(null) fell into that middle space of "well, you didn't really 
cover it, but it's such a silly case that I didn't want to make you cover it either, but 
here we are and we have to do something."  So maybe want some sort of 
SillyCaseException (perhaps with a less silly name) for     at least the null residue.
I believe that if Head and Tail exhaustively cover an enum or sealed type (as was 
the intended implication of my example)—more generally, in a situation that is 
optimistically total---then the user would be very happy to have some sort of error 
signaled if some other value shows up unexpectedly in a statement switch, whether 
that value is “Ankle" or “null”.  Maybe a new error name would be appropriate, 
such as UnexpectedNull.

If the user does not want such implicit handling of an optimistically total 
situation in a statement switch, then it is always possible to provide explicit 
clauses “case null: break;” and “default: break;”.

On the other hand, ICCE for Box(novel) does seem reasonable because the world 
really has changed in an incompatible way since the user wrote the code, and 
they probably do want to be alerted to the fact that their code is out of sync 
with the world.
Yep.

Separately (but not really separately), I'd like to refine my claim that 
`switch` is null-hostile.  In reality, `switch` NPEs on null in three cases: a 
null enum, String, or primitive box.  And, in each of these cases, it NPEs 
because (the implementation) really does dereference the target!  For a 
`String`, it calls `hashCode()`.  For an `enum`, it calls `ordinal()`.  And for 
a box, it calls `xxxValue()`.  It is _those_ methods that NPE, not the switch.  
(Yes, we could have designed it so that the implementation did a null check 
before calling those things.)

Reply via email to