Switch labels (null again), some tweaking

Maurizio Cimadamore maurizio.cimadamore at oracle.com
Wed Apr 28 18:09:46 UTC 2021


I think I got the two main fallacies which led me down the wrong path:

1. there is a distinction between patterns that are total on T, and 
patterns that are total on T, but with a "remainder"

2. there is a distinction between a pattern being total on T, and a set 
of patterns being total (or exhaustive) on T

This sent me completely haywire, because I was trying to reason in terms 
of what a plain pattern instanceof would consider "total", and then 
translate the results to nested patterns in switch - and that didn't 
work, because two different sets of rules apply there.

Maurizio

On 28/04/2021 18:27, Brian Goetz wrote:
> I think part of the problem is that we're using the word "total" in 
> different ways.
>
> A pattern P may be total on type T with remainder R.  For example, the 
> pattern `Soup s` is total on `Soup` with no remainder.
>
> A _set_ of patterns may be total on T with remainder R as well.  (The 
> only way a set of patterns is total is either (a) one of the patterns 
> in the set is already total on T, OR (b) sealing comes into play.)  
> Maybe this should be called "exhaustive" to separate from pattern 
> totality.
>
> Switch exhaustiveness derives from set-totality.
>
> Instanceof prohibits patterns that are total without remainder, for 
> two reasons: (1) its silly to ask a question which constant-folds to 
> `true`, and (b) the disagreement between traditional `instanceof 
> Object` and `instanceof <total pattern>` at null would likely be a 
> source of bugs.  (This was the cost of reusing instanceof rather than 
> creating a new "matches" operator.)
>
>> Foo x = ...
>> if (x instanceof Bar)
>>
>> The instanceof will not be considered total, and therefore be 
>> accepted by the compiler (sorry to repeat the same question - I want 
>> to make sure I understand how totality works with sealed hierarchies).
>>
>
> If the RHS of an `instanceof` is a type (not a type pattern), then 
> this has traditional `instanceof` behavior.  If Bar <: Foo, then this 
> is in effect a null check.
>
> If the RHS is a _pattern_, then the pattern must not be total without 
> remainder.  If Bar <: Foo, `Bar b` is total on Foo, so the compiler 
> says "dumb question, ask a different one."
>
> If the RHS is a non-total pattern, or a total pattern with remainder, 
> then there's a real question being asked.  So in your 
> Lunch-permits-Soup example, you could say
>
>     if (lunch instanceof Soup s)
>
> and this matches _on non-null lunch_.  Just like the switch. The only 
> difference is switch will throw on unmatched nulls, whereas instanceof 
> says "no, not an instance", but that's got nothing to do with 
> patterns, it's about conditional constructs.
>
>> If that's the case, I find that a bit odd - because enums kind of 
>> have the same issue (but we have opted to trust that a switch on an 
>> enum is total if all the constants known at compile time are covered) 
>> - and, to my eyes, if you squint, a sealed hierarchy is like an enum 
>> for types (e.g. sum type).
>>
>
> OK, let's talk about enums.  Suppose I have:
>
>     enum Lunch { Soup }
>
> and I do
>
>     switch (lunch) {
>         case Soup -> ...
>     }
>
> What happens on null?  It throws, and it always has.  The behavior for 
> the sealed analogue is the same; the `Soup s` pattern matches the 
> non-null lunches, and if null is left unhandled elsewhere in the 
> switch, the switch throws.  No asymmetry.
>
>> Anyway, backing up - this below:
>>
>> ```
>>
>> switch (lunch) {
>>         case Box(Soup s):
>>               System.err.println("Box of soup");
>>               break;
>>
>>         case Bag(Soup s):
>>              System.err.println("Bag of soup");
>>              break;
>>
>>         /* implicit */
>>         case Box(null), Bag(null): throw new NPE();
>>     }
>> ```
>>
>> is good code, which says what it means. I think the challenge will be 
>> to present error messages (e.g. if the user forgot to add case 
>> Box(null)) in a way that makes it clear to the user as to what's 
>> missing; and maybe that will be enough.
>>
>
> The challenge here is that we don't want to force the user to handle 
> the "silly" cases, such as:
>
>     Boolean bool = ...
>     switch (bool) {
>         case true -> ...
>         case false -> ...
>         case null -> ...  // who would be happy about having to write 
> this case?
>     }
>
> and the similarly lifted:
>
>     Box<Boolean> box = ...
>     switch (box) {
>         case Box(true) -> ...
>         case Box(false) -> ...
>         case Box(null) -> ...  // who would be happy about having to 
> write this case?
>         case Box(novel) -> ... // or this case?
>         case null ->           // or this case?
>     }
>
> So we define the "remainder" as the values that "fall into the cracks 
> between the patterns."  Users can write patterns for these, and 
> they'll match, but if not, the compiler inserts code to catch these 
> and throw something.
>
> The benefit is twofold: not only does the user not have to write the 
> stupid cases (imagine if Box had ten slots, would we want to write the 
> 2^10 partial null cases?), but because we throw on the remainder, DA 
> can treat the switch as covering all boxes, and be assured there are 
> no leaks.
>
-------------- next part --------------
An HTML attachment was scrubbed...
URL: <https://mail.openjdk.java.net/pipermail/amber-spec-experts/attachments/20210428/b7c70bd3/attachment-0001.htm>


More information about the amber-spec-experts mailing list