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