Nullable switch
Guy Steele
guy.steele at oracle.com
Fri Aug 7 18:17:59 UTC 2020
Okay, thanks. The trouble with using “Object” in this sort of discussion is that it is special in several ways that can be hard to tease apart. Going through the FrogBox example made the issues and distinctions involved, and the positions or designs being argued for, much clearer to me.
—Guy
> On Aug 7, 2020, at 10:48 AM, Brian Goetz <brian.goetz at oracle.com> wrote:
>
>
>> 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).
>
>
>
More information about the amber-spec-observers
mailing list