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