Nullity (was: Pattern features for next iteration)

Brian Goetz brian.goetz at
Thu Jan 21 18:52:39 UTC 2021

>    - New nullity behavior (including case null)

I think we can refine this item a bit, now that we've made some progress 
on guards.  There are still some mumbles of discomfort regarding the 
treatment of nulls.   We are never going to quiet those, because nulls 
are a persistent source of thorns, but I think we can add something new 
to the discussion in light of recent progress.  (But please, let's not 
just recycle old arguments.)

There are three contexts so far where we can put patterns:

  - RHS of an instanceof
  - case label of a switch
  - Nested inside a record or array pattern

Some of these have strong opinions on nulls, that might cause action to 
be taken before the pattern is even tested.  This is fine.  But we 
should be clear about which behaviors are part of the _construct_, vs 
which are part of pattern matching.  Currently:

  - instanceof always says false on null, no matter what
  - switch throws on null, except under circumstances we are defining now
  - nesting has no null opinions, but when the inner pattern requires a 
dynamic test (i.e., is not total), that pattern may have an opinion

With regard to what it means for "Pattern P to match target X whose 
static type is T", previous rounds came to a pretty solid conclusion 
that `var x` and `Object o` _must_ match null.    (So please, let's not 
reopen that unless there is something significantly new to add.)

What is missing is: when `Object o` in some context matches null, how do 
we express that we actually wanted to exclude null?  We've explored in 
the past some sort of `Object! o` type pattern, but resisted this for 
obvious stewardship reasons.  But the goal is valid: the pattern matches 
too much, and we want to refine the match.  And, this is what pattern 
composition does, so we should be looking to pattern composition to 
solve that.

Here's what is new: the treatment of guards as composable patterns. With 
that, we can write a "non-nullable" nested pattern like:

     case Foo(Object o & false(o == null)):


     case Foo(Object o) & false(o == null):

(depending on where the user thinks the null check is better.)  What I 
like here is that we haven't distorted the meaning of patterns to handle 
null specially, but instead are using ordinary composition mechanisms to 
allow users to filter nulls just like any other bad value.

If it turns out, that the world becomes full of such locutions, we can 
consider (in the future) adding a "null guard" pattern, `null(o)` and 
`non-null(o)`, at which point the above becomes:

     case Foo(Object o & non-null(o)):   // guard flavor


     case Foo(Object o & non-null()):       // targeted flavor

but we surely don't have to do that now, as this is just a trivial 
syntactic shortcut.

So, not only don't I think we have to add anything new now for handling 
nulls, or further distort the semantics of matching, but I see this as a 
dramatic validation of the guards-as-patterns strategy.  (Who knew 
composition was powerful!)

#### Special bonus nullity discussion

Separately, another possibility for switch is to slightly refine the 
null-handling of the switch _construct_ (not patterns), in a way that 
some people might find less surprising.

Problem: Switch has always been null-hostile, and pattern totality is 
subtle.  People seem very afraid (more than I think they should be, but 
fine) that the nulls will creep into the total patterns without warning.

Currently, we've defined that switch is null-hostile _unless_ there is a 
nullable case, and the two nullable cases are `case null` and `case 
<total pattern>`.  Obviously no one will be surprised to see a null 
match `case null`, but they might be surprised at the latter.

So the alternative idea to explore is: make `case null` the _only_ 
nullable case, but relax fallthrough to allow falling through from `case 
null` to a type pattern, and adjust the binding rules to make this make 
sense.  Then, a switch is only nullable if there is a case null (easy to 
spot) and a type test only sees null if the `case null` is highly proximate:

switch (o) {
     case Object o: // still NPEs on null, since no case null

switch (o) {
     case null -> doSomething();  // nulls here
     case Object o -> doSomethingElse(); // no nulls, already handled

switch (o) {
     case null:  // fall through
     case Object o: // nulls fall into this case, and are bound to o

As a bonus, it works not just for total type patterns, but lets us sort 
the nulls into any type pattern:

switch (o) {
     case String s -> ...
     case null, Integer i -> ...   // nulls go here
     case Object o -> ...

I'm OK with this because it seems a reasonable accommodation for the 
historical null-hostility of switch, but doesn't affect the semantics of 
patterns at all.

More information about the amber-spec-experts mailing list