Treatment of total patterns (was: Reviewing feedback on patterns in switch)
Remi Forax
forax at univ-mlv.fr
Tue Jan 25 23:03:53 UTC 2022
> From: "Brian Goetz" <brian.goetz at oracle.com>
> To: "amber-spec-experts" <amber-spec-experts at openjdk.java.net>
> Sent: Tuesday, January 25, 2022 8:47:09 PM
> Subject: Treatment of total patterns (was: Reviewing feedback on patterns in
> switch)
>> 1. Treatment of total patterns in switch / instanceof
> The handling of totality has been a long and painful discussion, trying to
> balance between where we want this feature to land in the long term, and
> people’s existing mental models of what switch and instanceof are supposed to
> do. Because we’ve made the conscious decision to rehabilitate switch rather
> than make a new construct (which would live side by side with the old construct
> forever), we have to take into account the preconceived mental models to a
> greater degree.
> Totality is a predicate on a pattern and the static type of its match target;
> for a pattern P to be total on T, it means that all values of T are matched by
> P. Note that when I say “matched by”, I am appealing not necessarily to “what
> does switch do” or “what does instanceof do”, but to an abstract notion of
> matching.
> The main place where there is a gap between pattern totality and whether a
> pattern matches in a switch has to do with null. We’ve done a nice job
> retrofitting “case null” onto switch (especially with `case null, Foo f` which
> allows the null to be bound to f), but people are still uncomfortable with
> `case Object o` binding null to o.
> (Another place there is a gap is with nested patterns; Box(Bag(String s)) should
> be total on Box<Bag<String>>, but can’t match Box(null). We don’t want to force
> users to add default cases, but a switch on Box<Bag<String>> would need an
> implicit throwing case to deal with the remainder.)
> I am not inclined to reopen the “should `Object o` be total” discussion; I
> really don’t think there’s anything new to say there. But we can refine the
> interaction between a total pattern and what the switch and instanceof
> constructs might do. Just because `Object o` is total on Object, doesn’t mean
> `case Object o` has to match it. It is the latter I am suggesting we might
> reopen.
> The motivation for treating total patterns as total (and therefore nullable) in
> switch comes in part from the desire to avoid introducing sharp edges into
> refactoring. Specifically, we had two invariants in mind:
> x matches P(Q) === x matches P(var alpha) && alpha matches Q:
> and
> switch (x) {
> case P(Q): A
> case P(T): B
> }
> where T is total on the type of x, should be equivalent to
> switch (x) {
> case P(var alpha):
> switch(alpha) {
> case Q: A
> case T: B
> }
> }
> }
> These invariants are powerful both for linguistic transformation and for
> refactoring.
> The refinements I propose are:
> - Null is only matched by a switch case that includes `case null`. Switches with
> no `case null` are treated as if they have a `case null: throw NPE`. This means
> that `case Object o` doesn’t match null; only `case null, Object o` does.
> - Total patterns are re-allowed in instanceof expressions, and are consistent
> with their legacy form.
> Essentially, this gives switch and instanceof a chance to treat null specially
> with their existing semantics, which takes precedence over the pattern match.
> The consequences of this for our refactoring rules are:
> - When unrolling a nested pattern P(Q), we can only do so when Q is not total.
> - When unrolling a switch over nested patterns to a nested switch, `case P(T)`
> must be unrolled not to `case T`, but `case null, T`.
> These changes entail no changes to the semantics of pattern matching; they are
> changes to the semantics of instanceof/switch with regard to null.
I have a slight preference for the C# syntax, the only way to have a total pattern is to use "var" so case P(T) is equivalent to instanceof P p && p.t instanceof T t.
Yes, it's not great because it means that "var" is not just inference but i think i prefer that compromise than having a type in a pattern means something different if it is nested or not.
The semantics you are proposing (or the one currently implemented in Java 18) is objectively neither worst nor better than the C# one, it's just different.
Pragmatically, we should choose the C# semantics, just because there are already thousands of people who knows it.
Rémi
-------------- next part --------------
An HTML attachment was scrubbed...
URL: <https://mail.openjdk.java.net/pipermail/amber-spec-experts/attachments/20220126/fbed4735/attachment-0001.htm>
More information about the amber-spec-experts
mailing list