[pattern-switch] Exhaustiveness

forax at univ-mlv.fr forax at univ-mlv.fr
Sat Aug 22 12:01:28 UTC 2020


> De: "Brian Goetz" <brian.goetz at oracle.com>
> À: "Guy Steele" <guy.steele at oracle.com>
> Cc: "Remi Forax" <forax at univ-mlv.fr>, "Tagir Valeev" <amaembo at gmail.com>,
> "amber-spec-experts" <amber-spec-experts at openjdk.java.net>
> Envoyé: Vendredi 21 Août 2020 17:14:57
> Objet: Re: [pattern-switch] Exhaustiveness

> 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.
I'm not hostile to that view, but may i ask an honest question, why this semantics is better ? 
Do you have examples where it makes sense to let the null to slip through the statement switch ? Because as i can see why being null hostile is a good default, it follows the motos "blow early, blow often" or "in case of doubt throws". 

Rémi 

[...] 

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

>>> On Aug 20, 2020, at 6:14 PM, Brian Goetz [ mailto:brian.goetz at oracle.com |
>>> <brian.goetz at 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.)
-------------- next part --------------
An HTML attachment was scrubbed...
URL: <https://mail.openjdk.java.net/pipermail/amber-spec-experts/attachments/20200822/b7ba064c/attachment.htm>


More information about the amber-spec-experts mailing list