On the last case being explicitly total

Brian Goetz brian.goetz at oracle.com
Tue Aug 11 22:12:56 UTC 2020


Thanks for clarifying that your sole concern here is that it seems too 
hard to tell, visually, whether the last case is total or not.

First, let me try to explain why this doesn't bother me so much. Here's 
one example, but there are more.

In a lot of code today that uses switches, nulls never get near a switch 
anyway -- otherwise we'd have many many more complaints about the NPEs 
that switches would be throwing.  But we don't, meaning the current uses 
of switch never see null in the first place.

But tomorrow, switches will get stronger.  People will want to replace 
chains of if-then-else like:

     if (x instanceof Fork) { A }
     else if (x instanceof Knife) { B }
     else { C }

with type switches:

     switch (x) {
         case Fork f: A
         case Knife k: B
         case Object o: C
     }

And guess what, the two have exactly the same semantics, down to the 
nullity behavior!  The old version dumped the nulls (if there were any) 
into the else clause, and the new version dumps them into the Object 
clause.  But if we keep the preemptive null-hostility of switch, then 
the above wouldn't work (though you could use `default` instead of 
Object.)  So what I think you object to is not relaxing the 
null-hostility, but relaxing it when there's no flashing red light that 
says "warning, total switch, new null semantics."  (I worry this is just 
Stroustrup's rule over again, though.)

The day after that, switches will get stronger again -- and be able to 
handle deconstruction and nested patterns.  And this is where it really 
pays off.  (But, if you don't like the totality-seeking behavior, any 
"fix" would have to cover the nested total pattern too.)

In general, I think total patterns _will_ look total, because they are 
intrinsically catch-alls.  I think we worry it will not because we 
haven't been programming at all with catch-all switch cases, because 
switch has historically been too weak to do this.   But once we get used 
to the patterns that naturally arise, I think we'll be happy and it 
won't be scary at all.

What about sealed types?  If we have a type sealed to A/B/C, then a 
switch with

     case A:
     case B:
     case C:

is total but NPEs on null.  That's probably OK, though, since its the 
same thing as a switch today does on enums.

In any case, I'm going to take an action item to write up the whole 
refactoring catalog -- just to make sure we are comfortable with any 
asymmetries that arise.

OK, now back to your story.

I rejected the "any X" approach because I think it is too error-prone -- 
it flips the default on what should be the catch-all case.  The natural 
thing is for

     case Box(var x):

to match all boxes.  Having to remember to specify a different ad-hoc 
syntax everywhere you think there might be a null is asking too much of 
users, they'll forget all the time and I don't think they'll thank us.  
It feels like the same "wrong default" as field accessibility and 
mutability today, but worse.

I rejected the "last case gets null" idea because we don't always want 
the last case to be a catch all!  And also, because we should be free to 
reorder type / total deconstruction patterns for unrelated types, but 
this would prevent that.   We want catch-all cases to be total, but not 
all switches have a catch-all, and we don't want to create subtle 
reordering anomalies that no one will ever remember.

Now you're suggesting that a total pattern must be "case var". 
Unfortunately that's not great either, because think about:

     switch (multiBox) {
         case MultiBox(String a, String b, String c): ...
         case MultiBox(Integer a, Integer b, Integer c): ...
         case var x: // are you kidding me?  I can't destructure here?
     }

Here, in the catch-all clause, we KNOW it is a multi-box; why can't we 
use a deconstruction pattern?  Isn't that what patterns are for? Maybe 
you will be OK if the pattern has all nested var clauses:

     case MultiBox(var x, var y, var z): ....

but my read is that you would still say that doesn't look "total" enough?

I think this is still coming at it from the wrong angle.  If the problem 
is that its not obvious that a switch is total (note that this is not a 
problem with expression switches -- they are always total, well, almost 
always), maybe you're really asking for something like (as we've discussed)

     total-switch (o) {
         // compiler, please type check that I cover the target type
     }

or, with a smaller hammer:

     switch (multiBox) {
         case MultiBox(String a, String b, String c): ...
         case MultiBox(Integer a, Integer b, Integer c): ...
         final case MultiBox(Object a, Object b, Object c): ...
     }

where `final case` would mean "this covers it all, its an error if it 
doesn't."  And then you are asking that any total non-exprssion switch 
without a `final case` (unless the last case is `var x`?) give an 
error.  That seems pretty fussy, but I might be talked into _allowing_ 
you to say `final case` (or `finally <pattern>`) as a way to force the 
totality type checking, and make the totality clear.

But, if that's what we're talking about, I'd prefer to keep that on the 
shelf as an option, rather than preemptively plunk for it now. We can 
always add it later compatibly, and I'm still not convinced this is 
remotely as big a problem as you think it is.  We knew back from the 
switch expression days that we might want to come back for a "check me 
for exhaustiveness please" option, and we still might.

> The problem: if there is no explicit "case null", the spec relies on the last case being total or not to accept null.
>
> The idea that relying only on the last case being total or not is a bad idea, because
> 1/ the syntax for a total case and a non total case are exactly the same while the semantics is not.
> 2/ knowing if something is total for a human is not obvious
> 3/ being total is not a local property.
>
> The proposed solution is necessarily "just a fix" because it's the only part of the spec that bug me.
>
> So i've unsuccessfully proposed several ways to make explicit that the case is total.
> First, using a special syntax to declare that a case is total, "case var|null" or "case any", or whatever syntax you find cool. Then i've proposed that the last case is always total because it's always like a cast and not like an instanceof.
>
> I'm now proposing to disallow the use of an explicit type in a total pattern and to ask users to use "case var" instead which is a restriction on the current proposed semantics.
> Using "case var" make it obvious that the pattern is total, it's syntactically different from the other case and it is a local property because the type is inferred as the same as the type switched upon (so even if the type switched upon changed, the case stay total).
>
> Rémi
>
>
>



More information about the amber-spec-experts mailing list