Next up for patterns: type patterns in switch

forax at univ-mlv.fr forax at univ-mlv.fr
Wed Aug 12 13:45:30 UTC 2020


> De: "Brian Goetz" <brian.goetz at oracle.com>
> À: "Guy Steele" <guy.steele at oracle.com>
> Cc: "Remi Forax" <forax at univ-mlv.fr>, "John Rose" <john.r.rose at oracle.com>,
> "amber-spec-experts" <amber-spec-experts at openjdk.java.net>
> Envoyé: Mercredi 12 Août 2020 00:43:59
> Objet: Re: Next up for patterns: type patterns in switch

> Let's assume that your trick works fine, Remi is happy, and `default P` means
> that P is total, that the switch is total, everything. Great.
yep, i'm hapy with "default P", let me explain why : 

I see the switch semantics as a kind of compact way to represent a cascade of if-else, 
with that they are several pattern 
case Constant is equivalent to if (o.equals(Constant)) or if (o == Constant) 
case null is equivalent to if (o == null) 
case P p is equivalent of if (o instanceof P p) 
and 
default P p is equivalent to else { P p = o; } 

some patterns accept null (case null and default P), so if one of them is present in a switch, the switch accept null, otherwise it does not. 

so the following code 
if (o instanceof Frog f) { ... } 
else if (o instanceof Chocolate c) { ... } 
else { var x = o; ... } 
can be refactored to 
switch(o) { 
case Frog f: ... 
case Chocolate c: ... 
default var x: ... 
} 

About exhaustiveness, if a switch is exhaustive, by example with 
sealed interface Container permits Box, Bag { } 
the following switch is exhaustive 
switch(container) { 
case Box box: ... 
case Bag bag: ... 
} 

Here there is no need for a total pattern and if a user want to allow null, he can add a "case null". 

> Now, what is the story for nested patterns?

> switch (container) {
> case Box(Frog f): ...
> case Box(Chocolate c): ...
> case Box(var x): ....

> case Bag(Frog f): ...
> case Bag(Chocolate c): ...
> case Bag(var x): ....

> }
so this is a mix between an exhaustive switch with two total patterns once de-constructed, for me, it should be written like this 
switch(container) { 
case Box(Frog g): ... 
case Box(Chocolate c): ... 
default Box(var x): ... 

case Bag(Frog g): ... 
case Bag(Chocolate c): ... 
default Bag(var x): ... 
} 

using the syntax "default Box(var x)" to say that the nested-patterns are locally total thus accept null. 
It's a little weird to have the "default" in front of the type name while it applies on the nested part but i'm Ok with that. 

> We still have totality at the nested level; when container is a box or a bag,
> the catch-all case is total on that kind of container, but no one has said
> "default" or "total" or anything like that. And a Box(null) will get dumped
> into the third case. Do we care?

> I don't, really, but you knew that. Remi might, but we'll have to hear from him.
> But I offer this example as a bisection to determine whether the discomfort is
> really about totality-therefore-nullable in patterns, or really just about
> exhaustive switches (and the nullity thing is a red herring.)
I think it's a mix, at top-level it's an exhaustive switch so not nullable but if Box and the Bag may contains null, "Box(var x)" and "Bag(var x)" should use a default pattern because there a kind of locally total. 

> So, who is bothered by the fact that case #3 gets Box(null), and case #6 gets
> Bag(null)? Anyone? (And, if not, but you are bothered by the lack of totality
> on the true catch-alls, why not?)
I'm bothered if the pattern are not declared as total and i believe Stephen Colebourne on amber-dev is proposing exactly the same rules. 

Rémi 

> On 8/11/2020 6:37 PM, Guy Steele wrote:

>>> On Aug 11, 2020, at 6:26 PM, Brian Goetz < [ mailto:brian.goetz at oracle.com |
>>> brian.goetz at oracle.com ] > wrote:

>>>> On the other hand, I think Remi’s point about totality being an implicit and
>>>> non-local property that is easily undermined by code changes in another
>>>> compilation unit is worrisome.

>>> ... which in turn derives from something else worrisome (a problem we bought
>>> last year): that it is not clear from looking at a switch whether it is
>>> exhaustive or not. Expression switches must exhaustive, but statement switches
>>> need not be. Here, we are saying that exhaustive switch statements are a useful
>>> new thing (which they are) and get rewarded with new behaviors (some may not
>>> find it a reward), but you have to look too closely to determine whether the
>>> switch is total. If it is, the last clause is total too (well, unless it is an
>>> enum switch that names all the constants, or a switch over a sealed type that
>>> names all the sub-types but without a catch-all.)

>>> So I claim that, if there is a problem, it is that it should be more obvious
>>> that a switch is exhaustive on its target.

>>>> Putting this all together, I reach two conclusions:

>>>> (1) We can live with the current definition of instanceof, provided we make it
>>>> clear that instanceof is not purely equivalent to pattern matching, and that
>>>> instanceof and pattern matching can be simply defined in terms of each other.

>>> I think we can do slightly better than this. I argue that

>>> x instanceof null

>>> is silly because we can just say

>>> x == null

>>> instead (which is more direct), and similarly

>>> x instanceof var y
>>> x instance of Object o

>>> are silly because we can just say

>>> var y = x

>>> instead (again more direct). So let's just ban the nullable patterns in
>>> instanceof, and no one will ever notice.

>>>> (2) We have a real disagreement about switch, but I think the fault lies in the
>>>> design of switch rather than with pattern matching, and the fault is this:

>>>> Sometimes when we write

>>>> switch (v) {
>>>> case Type1 x: A
>>>> case Type2 y: B
>>>> case Type3 z: C
>>>> }

>>>> we mean for Type1 and Type 2 and Type3 to be three disparate and co-equal
>>>> things—in which case it seems absurd for any of them to match null; but other
>>>> times we mean for Type3 to be a catchall, in which case we do want it to match
>>>> null if nothing before it has.

>>> Agreed. The fundamental concern that Remi (and Stephen, over on a-dev) have
>>> raised is that we can't tell which it is, and that is disturbing. (I still
>>> think it won't matter in reality, but I understand the concern.) The same
>>> ambiguity happens with deconstruction patterns:

>>> case Type3(var x, var y, var z) t3: ...

>>> which we can think of as "enhanced" type patterns.

>> Sure.

>>> (encouraging that our mails crossed with mostly the same observation and
>>> possible fix.)

>>>> I believe some previous discussion has focused on ways to modify the _pattern_
>>>> to indicated either an expectation of totality or a specific way of handling
>>>> null. But at this point I think augmenting patterns is overkill; what we need
>>>> (and all we need) is a modification to the syntax of switch to indicate an
>>>> expectation of totality. I have a modest suggestion:

>>>> switch (v) {
>>>> case Type1 x: A
>>>> case Type2 y: B
>>>> case Type3 z: C // Type3 is not expected to be a catchall
>>>> }

>>>> switch (v) {
>>>> case Type1 x: A
>>>> case Type2 y: B
>>>> default case Type3 z: C // Type3 is expected to be a catchall; it is a static
>>>> error if Type3 is not total on v,
>>>> // and Type3 will match null (unlike Type1 and Type2)
>>>> }

>>> And we already had another reason to want something like this: expression
>>> switches are exhaustive, statement switches are not, and we'd like to be able
>>> to engage the compiler to do exhaustiveness checking for statement switches
>>> even in the absence of patterns.

>>>> Now, I will admit that this syntax is a wee bit delicate, because adding a colon
>>>> might apparently change the meaning:

>>>> switch (v) {
>>>> case Type1 x: A
>>>> case Type2 y: B
>>>> default: case Type3 z: C
>>>> }

>>> Or `final case` or `finally <pattern>` or `default-case` or ...

>>> I am iffy about `default` because of its historical association, but I will have
>>> to re-think it in light of this idea before I have an opinion.

>> I don;t care about the syntax very much. I thought of “default” because it sort
>> of communicates the right idea and is already a keyword: it says that the last
>> clause is BOTH a case (with a pattern) but also a catchall.

>> I have to admit that “default case” (there is really no need for a hyphen here)
>> is a bit wordy compared to “finally”, which is very clever but could cause some
>> cognitive dissonance in users who think too hard about “try” (really? a case
>> clause that is always executed before you exit the switch??).

>>>> but I believe that in situations that matter, the compiler can and will reject
>>>> this last example on other grounds (please correct me if I am mistaken

>>> yes, the compiler can catch this.

>>> The other degree of freedom on this mini-feature is whether `default` is a hint,
>>> or whether it would be an error to not say `default` on a total pattern. I
>>> think it might be seen as a burden if it were required, but Remi might think it
>>> not strong enough if its just a hint.

>> Yeah, I thought about that, and decided that it would be a bad idea for the
>> compiler to complain about the absence of “default”, in part because you don't
>> want to feel vaguely obligated to include it in simple cases involving, for
>> example, exhaustive use of enums.
-------------- next part --------------
An HTML attachment was scrubbed...
URL: <https://mail.openjdk.java.net/pipermail/amber-spec-experts/attachments/20200812/663fd82e/attachment-0001.htm>


More information about the amber-spec-experts mailing list