Next up for patterns: type patterns in switch

Remi Forax forax at univ-mlv.fr
Tue Aug 11 12:27:50 UTC 2020


----- Mail original -----
> De: "John Rose" <john.r.rose at oracle.com>
> À: "Brian Goetz" <brian.goetz at oracle.com>
> Cc: "amber-spec-experts" <amber-spec-experts at openjdk.java.net>
> Envoyé: Mardi 11 Août 2020 00:20:42
> Objet: Re: Next up for patterns: type patterns in switch

> Letting the nulls flow is a good move in that absorbing
> game of “find the primitive”.  Here, you are observing forcefully
> that instanceof and switch, while important precedents and
> sources of use cases and design patterns, are not the primitives.
> We are not so lucky that the answer is “we just need more sugar
> for the existing constructs”.  But we are not so unlucky that
> we must build our new primitives out of alien materials.
> 
> The existing ideas about type matching, and specifically that
> `T x = v;` requires that `T` be total over the type of `v`,
> are available and useful.
> 
> Also useful is the idea that some constructs are necessarily
> null-hostile (starting with dot: `x.f`).  In a “let the nulls
> flow design”, if a construct is not *necessarily* null hostile,
> it is necessarily *null permissive*.  So we hunt for necessary
> hostility, and among patterns we find it with destructuring
> (`Box(var t)` as opposed to `Box`) and with value testing
> patterns like string or numeric literals (if those are patterns,
> which I think they should be).  We also note that the level
> of hostility (from patterns) must be compatible with
> generally null-agnostic use cases:  This means a pattern
> can fail on null (if it *must*) but it *must not throw on
> null*, because the next pattern in line might be the one
> that matches the null.
> 
> So instanceof and switch turn out to be sugar for certain
> uses of patterns.  And together they are universal enough
> that (luckily) we might not need a new syntax to directly
> denote the new primitive, of applying a pattern (partial
> or total) and extracting bindings.
> 
> The existing behavior w.r.t. nulls of instanceof and switch
> need to be rationalized.  I think that is easy, although there
> is a little bit to learn.  (Just as there’s something to learn
> today:  They are unconditionally null-rejecting at present.)
> An important thing (as Brian points out) is that if you
> are choosing to write null-agnostic code, your learning
> curve should be gentle-to-none.
> 
> Here are the rules the way I see them, in the presence
> of primitive patterns which are null-permissive (because
> they support null-agnostic use cases):
> 
> `x instanceof P` includes an additional check `x==null` before
> it tests the pattern P against x.  Rationale:  Compatibility.
> Also look at the name:  `null` is never an *instance* *of* any
> type.  A pattern might match null, but we are testing whether
> `x` is an instance, which is to say, an object.
> 
> Some equations to relate instanceof to the primitive __Matches:
> 
> x instanceof P  ≡  x __Matches P && (__PermitsNull(P) ? x != null : true)
> 
> x __Matches P  ≡  x instanceof P || (__PermitsNull(P) ? x == null : false)
> 
> Do we need syntax for __Matches P?  Probably not, because the
> above equations allow workarounds when the instanceof syntax
> isn’t exactly right.  (And it usually *is* exactly right; the trailing
> null logic folds away in context, or is harmless in some other way,
> as the nulls flow around.)
> 
> What about switch?  I like to think that a switch statement is simply
> sugar (plus optimizations) for a *decision chain*, an if/else chain which
> tests each case in turn (in source code order, of course):
> 
>   switch (x) {
>   case P: p(); break;
>   case Q: q(); break;
>>   default: d(); }
> 
> ⇒ (approximately)
> 
>  { var x_ = x;
>  if (x_ __Matches P) p(); else
>  if (x_ __Matches Q) q(); else
>>  d(); }
> 
> (Note that this account of classic switch requires extra tweaks
> to deal with two embarrassing features:  (a) fall through, which
> requires some way of contriving transfers between arms of the
> decision chain, and (b) the fact that default can go anywhere,
> and sometimes is placed in the middle to make use of fall-through.
> These are embarrassments, not show-stoppers.)
> 
> So what about nulls?  The simple—I will say naive—account
> of switch is that there is a null check at the head of the switch
> near `var x_ = x;`.  This would account for all of switch’s behaviors
> as of today, but makes switch hostile to nulls.
> 
> A more nuanced and useful account of switch’s behavior comes
> from the following observations:
> 
> 1. All switch cases *today*, if regarded as patterns, are necessarily
> null-rejecting.  *None of them ever match null.*
> 
> 2. The NPE observed from a switch-on-null, today, might as well
> be viewed as arising from the *bottom* of the decision chain,
> *after all matches* fail.  From that point of view, the fact that
> the failure appears to come from the *top* is simply an optimization,
> a fast-fail when it is statically provable that there’s no hope ever
> matching that pesky null, in any given legacy switch.
> 
> 3. When null meets default, we are painted into a corner, so we
> have to enjoy the only remaining option:  At least in legacy switches,
> the default case is *also* mandated to reject nulls.  (So “default”
> turns out to mean “anything but null”.  But that doesn’t parley
> into a general anti-null story; sorry null-haters.)  This feature
> of default can (maybe) be turned into a benefit:  Perhaps we
> can teach users that by saying “default” you are *asking* for
> an NPE, if a null escapes all the intervening patterns in the
> decision chain.  I don’t have a strong opinion on that.
> 
> The previous three observations fully account for today’s
> legacy switches, with their limited set of patterns.  The next
> one is also necessary to extend to switch cases which may
> support null-friendly patterns:
> 
> 4. We need a rule to allow nulls to flow through switches
> until the user is ready to handle them.  This means that
> null-permissive patterns in *some* switch cases need to
> be shielded from null just as with instanceof.
> 
> What is this rule?  We’ve already discussed it adequately;
> it comes in two parts:
> 
> A. `case null:` is allowed and does the obvious thing.
> We might as well require that it always come first.
> 
> B. There is a way of issuing a case which accepts nulls,
> and that way is a total pattern that is null friendly.
> (As Brian points out, this fits with the useful idea
> that a null-friendly pattern of the form `T v` or `var v`
> works just like the similar declaration.)
> 
> Note that B is less arbitrary than it might seem at first
> blush:  To avoid dead code, any total pattern in a switch
> must come *last*, at the bottom of the decision chain.
> (There can be no `default:` after it either, since that would
> be dead.)
> 
> So the rules together mean:
> 
> 1. If there is a `case null` at the top, that’s where nulls go.
> 2. If there is a total pattern at the bottom, that’s where nulls go.
> 3. Non-total patterns don’t catch nulls *in a switch*, just like in instanceof.
> 4. If there is neither a `case null` nor a total pattern, the switch throws NPE.

I'm proposing B. if the last case is a type pattern, then the switch is null friendly and this last case accept null.

I get,
1. If there is a `case null` at the top, that’s where nulls go.
2. If there is a type pattern at the bottom, that’s where nulls go.
4. If there is neither a `case null` nor a type pattern, the switch throws NPE

Rémi


More information about the amber-spec-experts mailing list