Switch labels (null again), some tweaking

John Rose john.r.rose at oracle.com
Fri Apr 23 19:27:40 UTC 2021


On Apr 23, 2021, at 8:38 AM, Brian Goetz <brian.goetz at oracle.com> wrote:
> 
> ...The fact is that the natural interpretation of a total type pattern is that it matches null.  People don't like this…

This person does.  As you point out, this position
enables composition of patterns.  And composition
is the more important goal, compared to preservation
of intuitions about edge cases.


> 
> So, given that a total type pattern matches null, but legacy switches reject null...
> 
> The treatment of the `null` label solves a few problems:
> 
>  - It lets people who want to treat null specially in switch do so, without having to do so outside the switch.  

Re. composition, it lets the switch be refactored into smaller
or larger switches while still managing its own business.
No need to bring in extra `if` statements.

>  - It lets us combine null handling with other things (case null, default:), and plays nicely when those other things have bindings (case null, String s:).
>  - It provides a visual cue to the reader that this switch is nullable.  
> 
> It is this last one that I think we may have over-rotated on.

I’m glad you are bringing this up. 

> In the treatment we've been discussing, we said:
> 
>  - switch always throws on null, unless there's a null label
> 
> Now, this is clearly appealing from a "how do I know if a switch throws NPE or not" perspective, so its understandable why this seemed a clever hack.

I agree with this goal, as a secondary goal.  But the
hack is not clever if it harms a more important goal.


>   ...It might be better to rip the band-aid off, and admit how patterns work.

(Because that’s the place Java will be in the long run.)

> Here's an example of the kind of mistake that this treatment encourages.

Good example.

>   ...The first switch does the right thing; the second will NPE on Foo(null).  And by insulating people from the real behavior of type patterns, it will be even more surprising when this happens.  

It’s possible to tweak the code to work around
the problem by adding `null,` on the last case.
And an IDE could do this.  But it’s a sharp edge
that comes from the extra interference of `switch`
into pattern semantics, which harms the primary
goal of composition.

> Now, let's look back at the alternative, where we keep the flexibility of the null label, but treat patterns as meaning what they mean, and letting switch decide to throw based on whether there is a nullable pattern or not.  So a switch with a total type pattern -- that is, `var x` or `Object x` -- will accept null, and thread it into the total case (which also must be the last case.) 

To me this is the material point, and has been all along:
There is never a need to perform an O(N) visual scan of
a switch to see if it accepts nulls, since the users simply
have to inspect the final (and perhaps initial) case of the
the switch.  Good style will avoid puzzlers such as final
cases which are difficult to classify (total vs. partial).
The language does not have to carry the burden of
enforcing full annotation.

A second point:  In the present design, and especially
with the cleanup you are proposing, I think there are
several possible (optionally selectable) styles of
null processing in switches.

(Reading later I see you already brought up most
of these points.)

Style A1 is implicit null acceptance.  Pattern
totality determines null acceptance, end of story.
Style A2 is implicit null rejection, with markup
of non-totality using `default`.  That is, a coder
could choose, as a matter of clarity, to call out
partial final cases by adding `default: break;`
after them.  (E-switches don’t need A2.)

Style B1 is explicit null acceptance.  If null
is allowed by the switch, then the token
`null` must be present, either at the head
of the switch or just before the final total
pattern.  Style B2 adds explicit null rejection,
by adding something like `case null: throw …`.
That’s probably too much noise in most cases.

The current draft mandates explicit null
acceptance.  Brian is suggesting that we
allow the other style too.  I think it’s a
good suggestion.

(An extra option would be to allow some
bespoke way to annotate totality and/or
non-totality of the final case.  That interacts
with things like enums and sealed classes,
so I’ll just mention it and move on.)

I think that IDEs can help users pick
the style that is correct for them.

Bottom line:  Trust the users to choose how
explicit to be with nulls.  More importantly,
trust them with compositional notations.


>  Who is this going to burn, that is not going to be burned by the existing switch behavior anyway?  I think very, very few people.  To get burned, a lot of things have to come together.  People are used to saying `default`; those that continue to are not going to get burned.

(Style A2.)

>   People are generally in agreement that `var x` should be total; people who use that are not going to get burned.  Switches today NPE eagerly on null, so having a null flow into code that doesn't expect it will result in ... the same NPE.  
> 
> And, people who want to be explicit can say:
> 
>     case null, Object o:

(Style B1.)

> and it will work -- and maybe even IntelliJ will hint them "hey, did you know this null is redundant?"  And then learning will happen!  

(Now I see I should have read ahead.  Oops.)

— John


More information about the amber-spec-experts mailing list