Nullable switch
Brian Goetz
brian.goetz at oracle.com
Fri Aug 7 14:48:13 UTC 2020
> Okay, so it would seem that we need two keywords (or other syntax) for
> use in patterns; I will temporarily call them “anything-but-null” and
> “anything-including-null”.
Not necessarily; the approach we've been driving towards has no (new)
keywords, and no _explicit_ consideration of nullability. There's just
type patterns, but their semantics take into account whether or not the
type pattern "covers" the target type. This is subtle, I grant, and I
can see where people would get confused, but it is far more
compositional and less ad-hoc.
Ignoring the epicyclical* distastefulness of the "any x" idea, I think
the the syntax issues are a bit of a red herring -- the issue is
structural. Under Remi's proposal, there is simply _no_ way to write a
switch where any number of cases covers "anything including null",
because the switch will throw before you get there:
switch (x) {
case String s:
case Object o:
}
would throw on NPE (as switches do today) before any cases are
considered, whether you say "var" or "any" or "Object." This is,
essentially, saying "switch is permanently polluted with null behavior,
we're not going to do anything about it, and anyone who wants to treat
nulls uniformly with other values just can't use switch (or has to
duplicate code, etc.) It also, as mentioned, provides pitfalls for
several expected-to-be-common refactorings, because the "obvious"
refactoring does not have the same semantics.
Instead, we are proposing to refine the handling of null in switch to
work in line with _totality_ of patterns -- building on the use of
totality in several other places. If we have a total pattern (like
`Object o`), we already use it for dead-code detection:
switch (x) {
case Object o: ...
case P: // error, dead code, no matter what P is
}
When we do pattern assignment, we use totality in flow analysis:
Point p = ...
...
Point(var x, var y) = p; // OK, Point(...) is total on Point
Object o = ...
...
Point(var x, var y) = o; // error, Point is not total on Object
Note that this definition of totality (so far) has a hole: nullity. We
can check the static type of the operand and verify that the pattern
covers all instances of that type, but without nullity in the type
system, we can't statically exclude null. So we propose to rectify
that: a total pattern matches null too. (You can consider `var x` to be
an "anything" pattern, or consider it to be inference for the obvious
type pattern; under this interpretation of total type patterns, the two
get you to the same semantics.) And this matches expected intuition (I
claim) for what "case Box(Object o)" at the end of a switch on boxes
should do -- but gets there entirely in terms of mechanical composition
rules for patterns.
We can only use patterns in switch and instanceof, but both of these
constructs currently have fail-fast behaviors with null. So we are
proposing to refine the null handling of switch (compatibly) so that we
can work with, rather than against, totality.
*Celestial term for "bag nailed on the side"
> In particular, I am uncertain about this situation: suppose we have a
> pattern FrogBox(Frog x) (that is, it is known that the argument to
> FrogBox must be of type Frog), and Tadpole is a class that extends (or
> implements) Frog; then consider
>
> FrogBox fb = …;
> switch (fb) {
> case FrogBox(Tadpole x) -> … ;
> case FrogBox(Frog x) -> … ;
> }
>
> (I think this is similar to previous examples, but I want the catchall
> case to be Frog, not Object.) Is the preceding switch total, or do I
> need to say either
Yes. Let's write out the declaration of the FrogBox pattern to see.
class FrogBox {
Frog f;
deconstructor FrogBox(Frog f) {
f = this.frog;
}
}
For
case FrogBox(Frog x):
we do overload resolution to find the deconstruction pattern, and
discover that its binding is of type Frog. The nested pattern `Frog x`
is total on `Frog`, and the deconstruction pattern FrogBox(Q) is total
on FrogBox when Q is total on Frog, and therefore the compound pattern
`FrogBox(Frog x)` is total on FrogBox (under the working proposal.)
Under Remi's proposal, it is not; it exlcudes FrogBox(null). (Also, if
this were a switch expression, you'd be told it is not exhaustive.)
Further, under Remi's proposal, you can't easily refactor into a nested
switch, because that would change the null behavior from "ignore" to
"throw", unless you added an extra "if x == null".
If you wanted to be total under Remi's proposal, you'd have to change
the last case to
case FrogBox(any x):
But you'd still have no way to refactor to a nested switch without
manually handling the null that pops out, and duplicating the code for
FrogBox(null) and FrogBox(Frog).
-------------- next part --------------
An HTML attachment was scrubbed...
URL: <https://mail.openjdk.java.net/pipermail/amber-spec-experts/attachments/20200807/e8660a25/attachment.htm>
More information about the amber-spec-experts
mailing list