> De: "Brian Goetz" <brian.go...@oracle.com> > À: "Guy Steele" <guy.ste...@oracle.com> > Cc: "Remi Forax" <fo...@univ-mlv.fr>, "John Rose" <john.r.r...@oracle.com>, > "amber-spec-experts" <amber-spec-experts@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.go...@oracle.com | >>> brian.go...@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.