From brian.goetz at oracle.com Thu Apr 1 13:23:28 2021 From: brian.goetz at oracle.com (Brian Goetz) Date: Thu, 1 Apr 2021 09:23:28 -0400 Subject: Possible refinement for sealed classes Message-ID: Been working with sealed classes a lot lately, and have run into a pattern that we might want to consider supporting more directly, which is interfaces that exist solely to refine a sealed class or interface. Suppose we have: ??? sealed abstract class Sandwich { } ??? interface WithCheese { } ??? interface Toasted { } ??? interface NontraditionalStructure { } ??? interface NontraditionalIngredients { } ??? class Cheeseburger extends Sandwich implements WithCheese { } ??? class TunaMelt extends Sandwich implements Toasted, WithCheese { } ??? class Burrito extends Sandwich implements WithCheese, NontraditionalStructure { } ??? class ChipButty extends Sandwich implements NontraditionalIngredients { } ??? class PopTart extends Sandwich implements NontraditionalIngredients, NonTraditionalStructure { } (see, e.g., https://twitter.com/matttomic/status/859117370455060481, for those who don't get the joke.) The constraint we would like to express here is that the interfaces WithCheese and Toasted exist to serve Sandwich, which is extension-controlled.? To capture this, we also seal these interfaces, but now have to redundantly enumerate their subtypes.? This is annoying and brittle, because we're stating it indirectly; a more direct capture of intent would be to say "Toasted can only be used with Sandwich." Even if Sandwich were an interface, and then we can say "Toasted extends Sandwich", we still have to redundantly enumerate the subtypes.? If this game is about Sandwich, just having Sandwich enumerate its subtypes should be enough. Without trying to paint the bikeshed, this is a pretty simple extension to sealing: ??? sealed interface WithCheese __refines Sandwich { } This says that WithCheese is sealed to either classes that extend Sandwich, or other interfaces that __refines Sandwich.? As a bonus, default methods in WithCheese can refer to methods in Sandwich, since it must always be the case that `this instanceof Sandwich`. Essentially, I want to put the `permits` list on Sandwich, and have `WithCheese` and friends delegate their permits to `Sandwich`, rather than having each interface enumerate their subtypes. -------------- next part -------------- An HTML attachment was scrubbed... URL: From heidinga at redhat.com Thu Apr 1 13:34:59 2021 From: heidinga at redhat.com (Dan Heidinga) Date: Thu, 1 Apr 2021 09:34:59 -0400 Subject: Possible refinement for sealed classes In-Reply-To: References: Message-ID: Seems like a reasonable proposal. > sealed interface WithCheese __refines Sandwich { } Would `_refines` operate at compile time with javac copy/pasting the sealed list from Sandwich into WithCheese? An alternative is some new classfile info to tell the VM to do the copy/paste at classload. Both are possible but the VM option is more expensive to implement. --Dan On Thu, Apr 1, 2021 at 9:24 AM Brian Goetz wrote: > > Been working with sealed classes a lot lately, and have run into a pattern that we might want to consider supporting more directly, which is interfaces that exist solely to refine a sealed class or interface. Suppose we have: > > sealed abstract class Sandwich { } > > interface WithCheese { } > interface Toasted { } > interface NontraditionalStructure { } > interface NontraditionalIngredients { } > > class Cheeseburger extends Sandwich implements WithCheese { } > > class TunaMelt extends Sandwich implements Toasted, WithCheese { } > > class Burrito extends Sandwich implements WithCheese, NontraditionalStructure { } > > class ChipButty extends Sandwich implements NontraditionalIngredients { } > > class PopTart extends Sandwich implements NontraditionalIngredients, NonTraditionalStructure { } > > (see, e.g., https://twitter.com/matttomic/status/859117370455060481, for those who don't get the joke.) > > The constraint we would like to express here is that the interfaces WithCheese and Toasted exist to serve Sandwich, which is extension-controlled. To capture this, we also seal these interfaces, but now have to redundantly enumerate their subtypes. This is annoying and brittle, because we're stating it indirectly; a more direct capture of intent would be to say "Toasted can only be used with Sandwich." > > Even if Sandwich were an interface, and then we can say "Toasted extends Sandwich", we still have to redundantly enumerate the subtypes. If this game is about Sandwich, just having Sandwich enumerate its subtypes should be enough. > > Without trying to paint the bikeshed, this is a pretty simple extension to sealing: > > sealed interface WithCheese __refines Sandwich { } > > This says that WithCheese is sealed to either classes that extend Sandwich, or other interfaces that __refines Sandwich. As a bonus, default methods in WithCheese can refer to methods in Sandwich, since it must always be the case that `this instanceof Sandwich`. > > Essentially, I want to put the `permits` list on Sandwich, and have `WithCheese` and friends delegate their permits to `Sandwich`, rather than having each interface enumerate their subtypes. From brian.goetz at oracle.com Thu Apr 1 13:50:06 2021 From: brian.goetz at oracle.com (Brian Goetz) Date: Thu, 1 Apr 2021 09:50:06 -0400 Subject: [External] : Re: Possible refinement for sealed classes In-Reply-To: References: Message-ID: <64ae6d9b-bd18-5144-b0e1-11f205e55962@oracle.com> It depends how far we want to take this.? If we limit this to sealed classes, we can just do as you say, copy the permitted subclasses of Sandwich into the permitted subclasses of WithCheese, possibly filtering for whether with Cheese is actually a supertype.? (If we want to capture the notion that "my implementors must also extend Sandwich", but not limit it to sealed classes, then we need a new kind of attribute to enforce this.) We could write this as: ??? sealed interface WithCheese ??????? permits extends Sandwich { } and require that Sandwich be sealed. On 4/1/2021 9:34 AM, Dan Heidinga wrote: > Seems like a reasonable proposal. > >> sealed interface WithCheese __refines Sandwich { } > Would `_refines` operate at compile time with javac copy/pasting the > sealed list from Sandwich into WithCheese? An alternative is some new > classfile info to tell the VM to do the copy/paste at classload. Both > are possible but the VM option is more expensive to implement. > > --Dan > > On Thu, Apr 1, 2021 at 9:24 AM Brian Goetz wrote: >> Been working with sealed classes a lot lately, and have run into a pattern that we might want to consider supporting more directly, which is interfaces that exist solely to refine a sealed class or interface. Suppose we have: >> >> sealed abstract class Sandwich { } >> >> interface WithCheese { } >> interface Toasted { } >> interface NontraditionalStructure { } >> interface NontraditionalIngredients { } >> >> class Cheeseburger extends Sandwich implements WithCheese { } >> >> class TunaMelt extends Sandwich implements Toasted, WithCheese { } >> >> class Burrito extends Sandwich implements WithCheese, NontraditionalStructure { } >> >> class ChipButty extends Sandwich implements NontraditionalIngredients { } >> >> class PopTart extends Sandwich implements NontraditionalIngredients, NonTraditionalStructure { } >> >> (see, e.g., https://urldefense.com/v3/__https://twitter.com/matttomic/status/859117370455060481__;!!GqivPVa7Brio!MsQV5tDsdWajfqsD3USkhms4IepmBckE_aLEMz1w6TGLcSGGhLvNWLwamCE2XSHtwg$ , for those who don't get the joke.) >> >> The constraint we would like to express here is that the interfaces WithCheese and Toasted exist to serve Sandwich, which is extension-controlled. To capture this, we also seal these interfaces, but now have to redundantly enumerate their subtypes. This is annoying and brittle, because we're stating it indirectly; a more direct capture of intent would be to say "Toasted can only be used with Sandwich." >> >> Even if Sandwich were an interface, and then we can say "Toasted extends Sandwich", we still have to redundantly enumerate the subtypes. If this game is about Sandwich, just having Sandwich enumerate its subtypes should be enough. >> >> Without trying to paint the bikeshed, this is a pretty simple extension to sealing: >> >> sealed interface WithCheese __refines Sandwich { } >> >> This says that WithCheese is sealed to either classes that extend Sandwich, or other interfaces that __refines Sandwich. As a bonus, default methods in WithCheese can refer to methods in Sandwich, since it must always be the case that `this instanceof Sandwich`. >> >> Essentially, I want to put the `permits` list on Sandwich, and have `WithCheese` and friends delegate their permits to `Sandwich`, rather than having each interface enumerate their subtypes. -------------- next part -------------- An HTML attachment was scrubbed... URL: From forax at univ-mlv.fr Thu Apr 1 14:18:02 2021 From: forax at univ-mlv.fr (Remi Forax) Date: Thu, 1 Apr 2021 16:18:02 +0200 (CEST) Subject: Possible refinement for sealed classes In-Reply-To: References: Message-ID: <734287350.785566.1617286682225.JavaMail.zimbra@u-pem.fr> > De: "Brian Goetz" > ?: "amber-spec-experts" > Envoy?: Jeudi 1 Avril 2021 15:23:28 > Objet: Possible refinement for sealed classes > Been working with sealed classes a lot lately, and have run into a pattern that > we might want to consider supporting more directly, which is interfaces that > exist solely to refine a sealed class or interface. Suppose we have: > sealed abstract class Sandwich { } > interface WithCheese { } > interface Toasted { } > interface NontraditionalStructure { } > interface NontraditionalIngredients { } > class Cheeseburger extends Sandwich implements WithCheese { } > class TunaMelt extends Sandwich implements Toasted, WithCheese { } > class Burrito extends Sandwich implements WithCheese, NontraditionalStructure { > } > class ChipButty extends Sandwich implements NontraditionalIngredients { } > class PopTart extends Sandwich implements NontraditionalIngredients, > NonTraditionalStructure { } > (see, e.g., [ https://twitter.com/matttomic/status/859117370455060481 | > https://twitter.com/matttomic/status/859117370455060481 ] , for those who don't > get the joke.) > The constraint we would like to express here is that the interfaces WithCheese > and Toasted exist to serve Sandwich, which is extension-controlled. To capture > this, we also seal these interfaces, but now have to redundantly enumerate > their subtypes. This is annoying and brittle, because we're stating it > indirectly; a more direct capture of intent would be to say "Toasted can only > be used with Sandwich." > Even if Sandwich were an interface, and then we can say "Toasted extends > Sandwich", we still have to redundantly enumerate the subtypes. If this game is > about Sandwich, just having Sandwich enumerate its subtypes should be enough. > Without trying to paint the bikeshed, this is a pretty simple extension to > sealing: > sealed interface WithCheese __refines Sandwich { } > This says that WithCheese is sealed to either classes that extend Sandwich, or > other interfaces that __refines Sandwich. As a bonus, default methods in > WithCheese can refer to methods in Sandwich, since it must always be the case > that `this instanceof Sandwich`. > Essentially, I want to put the `permits` list on Sandwich, and have `WithCheese` > and friends delegate their permits to `Sandwich`, rather than having each > interface enumerate their subtypes. I'm not sure to understand why this case is special enough to get a special way of delegating the "sealness" (the fact to be sealed, i don't know if there is a term in English for that) apart that it would be cool to have it. Delegating the "sealness" in that direction, from an interface which subtypes are subtypes to the sealed type, seems less readable that the other direction, from the sealed type to the interface that will have subtypes that implement the sealed type. So is it equivalent to sealed interface Sandwich permits __relaxed WithCheese, WithToast, etc with __relaxed meaning that WithCheese doesn't have to be a subtype of Sandwich, but the subtypes of WithCheese, etc have to ? If Sandwich see WithCheese but not the subtypes of WitchCheese, it can still be sealed with those types ? How it works in term of separate compilation, if I add "__refines Sandwich" to a WithCheese without recompiling Sandwich ? R?mi -------------- next part -------------- An HTML attachment was scrubbed... URL: From brian.goetz at oracle.com Thu Apr 1 14:27:55 2021 From: brian.goetz at oracle.com (Brian Goetz) Date: Thu, 1 Apr 2021 10:27:55 -0400 Subject: [External] : Re: Possible refinement for sealed classes In-Reply-To: <734287350.785566.1617286682225.JavaMail.zimbra@u-pem.fr> References: <734287350.785566.1617286682225.JavaMail.zimbra@u-pem.fr> Message-ID: > > Essentially, I want to put the `permits` list on Sandwich, and > have `WithCheese` and friends delegate their permits to > `Sandwich`, rather than having each interface enumerate their > subtypes. > > > I'm not sure to understand why this case is special enough to get a > special way of delegating the "sealness" (the fact to be sealed, i > don't know if there is a term in English for that) apart that it would > be cool to have it. The tradeoff of sealing is that you have to maintain an explicit list of permitted subtypes (unless you're lucky enough that your whole hierarchy fits in one file.)? That's fine, but if you program in the Sandwich style outlined in this thread (which is a reasonable style for modeling some domains), you end up having to write O(n) permits lists, and keep them in sync.? This is annoying for both code writers and readers; gunking up the declaration of WithCheese (which is a pretty abstract concept) with a list of concrete cheese-bearing sandwiches feels out of place. > > Delegating the "sealness" in that direction, from an interface which > subtypes are subtypes to the sealed type, seems less readable that the > other direction, from the sealed type to the interface that will have > subtypes that implement the sealed type. Sure, there's other ways to paint the shed, but the concept here is: WithCheese only makes sense in the context of Sandwich (according to this sandwich restaurant.)? This is the concept we're trying to capture; it could be at the declaration of the interface, or at the declaration of Sandwich. -------------- next part -------------- An HTML attachment was scrubbed... URL: From guy.steele at oracle.com Thu Apr 1 15:30:31 2021 From: guy.steele at oracle.com (Guy Steele) Date: Thu, 1 Apr 2021 11:30:31 -0400 Subject: Possible refinement for sealed classes In-Reply-To: References: Message-ID: <80A04E4F-7516-42A4-806D-83CBEA5F3605@oracle.com> > On Apr 1, 2021, at 9:23 AM, Brian Goetz wrote: > > ... > Without trying to paint the bikeshed, this is a pretty simple extension to sealing: > > sealed interface WithCheese __refines Sandwich { } > > This says that WithCheese is sealed to either classes that extend Sandwich, or other interfaces that __refines Sandwich. As a bonus, default methods in WithCheese can refer to methods in Sandwich, since it must always be the case that `this instanceof Sandwich`. > > Essentially, I want to put the `permits` list on Sandwich, and have `WithCheese` and friends delegate their permits to `Sandwich`, rather than having each interface enumerate their subtypes. Maybe ?__refines? should be spelled ?extend?? If an interface J extends a class C, it means that it is a compile-time error if class D implements interface J but class D is not a subclass of C. Such a constraint might be useful even if C is not sealed. ?Guy -------------- next part -------------- An HTML attachment was scrubbed... URL: From forax at univ-mlv.fr Thu Apr 1 20:25:09 2021 From: forax at univ-mlv.fr (forax at univ-mlv.fr) Date: Thu, 1 Apr 2021 22:25:09 +0200 (CEST) Subject: [External] : Re: Possible refinement for sealed classes In-Reply-To: References: <734287350.785566.1617286682225.JavaMail.zimbra@u-pem.fr> Message-ID: <423630716.973546.1617308709605.JavaMail.zimbra@u-pem.fr> > De: "Brian Goetz" > ?: "Remi Forax" > Cc: "amber-spec-experts" > Envoy?: Jeudi 1 Avril 2021 16:27:55 > Objet: Re: [External] : Re: Possible refinement for sealed classes >>> Essentially, I want to put the `permits` list on Sandwich, and have `WithCheese` >>> and friends delegate their permits to `Sandwich`, rather than having each >>> interface enumerate their subtypes. >> I'm not sure to understand why this case is special enough to get a special way >> of delegating the "sealness" (the fact to be sealed, i don't know if there is a >> term in English for that) apart that it would be cool to have it. > The tradeoff of sealing is that you have to maintain an explicit list of > permitted subtypes (unless you're lucky enough that your whole hierarchy fits > in one file.) That's fine, but if you program in the Sandwich style outlined in > this thread (which is a reasonable style for modeling some domains), you end up > having to write O(n) permits lists, and keep them in sync. This is annoying for > both code writers and readers; gunking up the declaration of WithCheese (which > is a pretty abstract concept) with a list of concrete cheese-bearing sandwiches > feels out of place. I don't like too much the whole traits/mixins ideas because you share the implementation and the type at the same time, it's the same issue with inheritance, usually at some points you will discover that you don't want both. By example, a type WithCheese may make little sense. Anyway, if WithCheese only makes sense for a Sandwich, nesting WithCheese inside Sandwich is the way to go. >> Delegating the "sealness" in that direction, from an interface which subtypes >> are subtypes to the sealed type, seems less readable that the other direction, >> from the sealed type to the interface that will have subtypes that implement >> the sealed type. > Sure, there's other ways to paint the shed, but the concept here is: WithCheese > only makes sense in the context of Sandwich (according to this sandwich > restaurant.) This is the concept we're trying to capture; it could be at the > declaration of the interface, or at the declaration of Sandwich. As i said above, make WithCheese a nested interface of Sandwich, it shows that WithCheese only makes sense in the context of of Sandwich and you get permitted subtypes inference as a bonus. I believe that sealed traits in scale are restricted to the same compilation unit too. R?mi -------------- next part -------------- An HTML attachment was scrubbed... URL: From brian.goetz at oracle.com Thu Apr 1 20:32:23 2021 From: brian.goetz at oracle.com (Brian Goetz) Date: Thu, 1 Apr 2021 16:32:23 -0400 Subject: [External] : Re: Possible refinement for sealed classes In-Reply-To: <423630716.973546.1617308709605.JavaMail.zimbra@u-pem.fr> References: <734287350.785566.1617286682225.JavaMail.zimbra@u-pem.fr> <423630716.973546.1617308709605.JavaMail.zimbra@u-pem.fr> Message-ID: > By example, a type WithCheese may make little sense. > Anyway, if WithCheese only makes sense for a Sandwich, nesting > WithCheese inside Sandwich is the way to go. This doesn't help unless the *whole* hierarchy is nested in the same file.? Otherwise you have to have an explicit permits clause on each of Sandwich, WIthCheese, etc. From forax at univ-mlv.fr Thu Apr 1 21:11:48 2021 From: forax at univ-mlv.fr (forax at univ-mlv.fr) Date: Thu, 1 Apr 2021 23:11:48 +0200 (CEST) Subject: [External] : Re: Possible refinement for sealed classes In-Reply-To: References: <734287350.785566.1617286682225.JavaMail.zimbra@u-pem.fr> <423630716.973546.1617308709605.JavaMail.zimbra@u-pem.fr> Message-ID: <1884140974.983522.1617311508175.JavaMail.zimbra@u-pem.fr> ----- Mail original ----- > De: "Brian Goetz" > ?: "Remi Forax" > Cc: "amber-spec-experts" > Envoy?: Jeudi 1 Avril 2021 22:32:23 > Objet: Re: [External] : Re: Possible refinement for sealed classes >> By example, a type WithCheese may make little sense. >> Anyway, if WithCheese only makes sense for a Sandwich, nesting >> WithCheese inside Sandwich is the way to go. > > This doesn't help unless the *whole* hierarchy is nested in the same > file.? Otherwise you have to have an explicit permits clause on each of > Sandwich, WIthCheese, etc. The whole public hierarchy, you can still have an interface inside the compilation unit and a non visible implementation in another file. R?mi From gavin.bierman at oracle.com Tue Apr 20 09:00:34 2021 From: gavin.bierman at oracle.com (Gavin Bierman) Date: Tue, 20 Apr 2021 09:00:34 +0000 Subject: [sealed-types] Draft Spec for JEP 409 Sealed Types (Final) Message-ID: Dear experts: Draft of the specs for JEP 409 (Sealed Types) are available at: http://cr.openjdk.java.net/~gbierman/jep409/latest/ Comments welcome! Thanks, Gavin From brian.goetz at oracle.com Fri Apr 23 15:38:42 2021 From: brian.goetz at oracle.com (Brian Goetz) Date: Fri, 23 Apr 2021 11:38:42 -0400 Subject: Switch labels (null again), some tweaking In-Reply-To: References: Message-ID: <9ca67b52-70a1-b0a3-7da0-da96fa5d80ab@oracle.com> Gavin has a spec that captures most of what we've talked about here, which should be posted soon.? But I want to revisit one thing, because I think we may have swung a little too eagerly at a bad pitch. There is a distinction between the _semantics of a given pattern_ and _what a given construct might do with that pattern_.? The construct (instanceof, switch) gets first crack at having an opinion, and then may choose to evaluate whether the pattern matches something.? The place where we have been most tempted to use this flexibility is with "null", because null ruins everything. We made a decision to lump pattern matching in with `instanceof` because it seemed silly to have two almost identical but subtly different constructs for "dynamic type test" and "pattern match" (given that you can do dynamic type tests with patterns.)? We knew that this would have some uncomfortable consequences, and what we have tentatively decided to do is outlaw total patterns in instanceof, so that users are not confronted with the subtle difference between `x instanceof Object` and `x instanceof Object o`.? This may not be a totally satisfying answer, and we left some room to adjust this, but its where we are. The fact is that the natural interpretation of a total type pattern is that it matches null.? People don't like this, partially because it goes against the intuition that's been built up by instanceof and switch.? (If we have to, I'm willing to lay out the argument again, but after having been through it umpteen times, I don't see any of the alternatives as being better.) So, given that a total type pattern matches null, but legacy switches reject null... The treatment of the `null` label solves a few problems: ?- It lets people who want to treat null specially in switch do so, without having to do so outside the switch. ?- It lets us combine null handling with other things (case null, default:), and plays nicely when those other things have bindings (case null, String s:). ?- It provides a visual cue to the reader that this switch is nullable. It is this last one that I think we may have over-rotated on. In the treatment we've been discussing, we said: ?- switch always throws on null, unless there's a null label Now, this is clearly appealing from a "how do I know if a switch throws NPE or not" perspective, so its understandable why this seemed a clever hack.? But reading the responses again: Tagir: > I support making case null the only null-friendly pattern. Maurizio: > there's no subtle type dependency analysis which determines the fate of null it seems that people wanted to read way more into this than there was; they wanted this to be a statement about patterns, not about switch.? I think this is yet another example of the "I hate null so much I'm willing to take anything to make it (appear to) go away" biases we all have."? Only Remi seemed to have recognized this for the blatant trick it is -- we're hiding the null-accepting behavior of total patterns until people encounter them with nested patterns, where it will be less uncomfortable. To be clear: this is *not* making `null` the only null-friendly pattern.? But these responses make me worry that, if the experts can't tell the difference, then no user will be able to tell the difference, and that this is just kicking the confusion down the road.? It might be better to rip the band-aid off, and admit how patterns work. Here's an example of the kind of mistake that this treatment encourages.? If we have: ??? switch (x) { ??????? case Foo(String a):? A ??? ??? case Foo(Integer a): B ??????? case Foo(Object a):? C ??? } and we want to refactor to switch (x) { ??????? case Foo(var a): ??? ?? ???? switch(a) { ??????????????? case String a:? A ??????????????? case Integer a: B ??????????????? case Object a:? C ??????????? } ??? } we've made a mistake.? The first switch does the right thing; the second will NPE on Foo(null).? And by insulating people from the real behavior of type patterns, it will be even more surprising when this happens. Now, let's look back at the alternative, where we keep the flexibility of the null label, but treat patterns as meaning what they mean, and letting switch decide to throw based on whether there is a nullable pattern or not.? So a switch with a total type pattern -- that is, `var x` or `Object x` -- will accept null, and thread it into the total case (which also must be the last case.) Who is this going to burn, that is not going to be burned by the existing switch behavior anyway?? I think very, very few people.? To get burned, a lot of things have to come together.? People are used to saying `default`; those that continue to are not going to get burned.? People are generally in agreement that `var x` should be total; people who use that are not going to get burned.? Switches today NPE eagerly on null, so having a null flow into code that doesn't expect it will result in ... the same NPE. And, people who want to be explicit can say: ??? case null, Object o: and it will work -- and maybe even IntelliJ will hint them "hey, did you know this null is redundant?"? And then learning will happen! So, I think the "a switch only accepts null if the letters n-u-l-l are present", while a comforting move in the short term, buys us relatively little, and dulls our pain receptors which in turn makes it take way longer to learn how patterns really work.? I think we should go back to: ?- A switch accepts null if (a) one of the case labels is `null` or (b) the switch has a total pattern (which must always be the last case.) On 3/12/2021 9:12 AM, Brian Goetz wrote: > The JEP has some examples of how the `null` case label can combine > with others.? But I would like to propose a more general way to > describe what's going on.? This doesn't change the proposed language > (much), as much as describing/specifying it in a more general way. > > We have the following kinds of switch labels: > > ??? case > ??? case null > ??? case > ??? default > > The question is, which can be combined with each other into a single > case, such as: > > ??? case 3, null, 5: > > This question is related to, which can fall into each other: > > ??? case 3: > ??? case null: > ??? case 5: > > We can say that certain labels are compatible with certain others, and > ones that are compatible can be combined / are candidates for > fallthrough, by defining a compatibility predicate: > > ?- All case labels are compatible with each other; > ?- The `null` label is compatible with labels; > ?- The `null` label is compatible with `default`; > ?- The `null` label is compatible with (explicit) type patterns: > > (There is already a check that each label is applicable to the type of > the target.) > > Then we say: you can combine N case labels as long as they are all > compatible with each other.? Combination includes both comma-separated > lists in one case, as well as when one case is _reachable_ from > another (fall through.)? And the two can be combined: > > ??? case 3, 4: // fall through > ??? case null: > > So the following are allowed: > > ??? case 1, 2, 3, null: > > ??? case null, 1, 2, 3: > > ??? case null, Object o: > > ??? case Object o, null: > > ??? case null, default:??? // special syntax rule for combining default > > The following special rules apply: > > ?- `default` can be used as a case label when combined with compatible > case labels (see last example above); > ?- When `null` combines with a type pattern, the binding variable of > the type pattern can bind null. > > The semantics outlined in Gavin's JEP are unchanged; this is just a > new and less fussy way to describe the behavior of the null label / > specify the interaction with fallthrough. > > > > -------------- next part -------------- An HTML attachment was scrubbed... URL: From gavin.bierman at oracle.com Fri Apr 23 15:58:38 2021 From: gavin.bierman at oracle.com (Gavin Bierman) Date: Fri, 23 Apr 2021 15:58:38 +0000 Subject: [patterns-switch] Draft JLS spec for JEP 406 (Pattern Matching for Switch) Message-ID: <498B5850-1209-4776-A287-E7704087B7A1@oracle.com> Dear experts, Apologies for the delay, but here is the first draft of the spec for JEP 406 (Pattern Matching for Switch): http://cr.openjdk.java.net/~gbierman/jep406/latest/ As you will see, the design has evolved a little since we wrote the JEP (which I will update shortly). The chief changes are: 1. Instead of enumerating the special case labels, e.g. case null, default, case null, T t and so on; the notion of switch label has been made more general, with a set of restrictions to cut out the things we don?t want. In particular, this means that we get (a) symmetric versions of the compound patterns, e.g. case null, default and case default, null: and (b) we get a perfect alignment of the old : style with the new , style, e.g. we can write both `case null, default: ?` and `case null: default: ?` with identical treatment. 2. I figured out a way to remove the restriction from the JEP that switches must be all pattern-matching, or all non-pattern-matching. Apart from simplifying matters, this allows for some nice new features, e.g. enum E { F, G, H } switch(...) { // A switch block with an enum constant and pattern label! case F -> ? case E e -> ? // Acts like a default but e is in scope! } A couple of odd extras come for the ride, just for your information: switch(o) { /* empty */ } is now well-typed for all types of o, as is: switch(o) { default: ? } [Basically any type restrictions on the selector expression come from the switch labels. We still maintain that if you use a constant case label, the type of the selector must be char, byte, short, int, Character, Byte, Short, Integer, or String as in Java 16.] As always, your opinions are welcomed! Thanks, Gavin From brian.goetz at oracle.com Fri Apr 23 18:24:40 2021 From: brian.goetz at oracle.com (Brian Goetz) Date: Fri, 23 Apr 2021 14:24:40 -0400 Subject: [sealed-types] Draft Spec for JEP 409 Sealed Types (Final) In-Reply-To: References: Message-ID: <9944d69a-cf12-ae91-e5b7-01eaa895b500@oracle.com> Very nice how the restrictions on primitive narrowing shook out. In 8.1.1.2, you mention that JEP 395 proposes records, but records are now a permanent feature. On 4/20/2021 5:00 AM, Gavin Bierman wrote: > Dear experts: > > Draft of the specs for JEP 409 (Sealed Types) are available at: > > http://cr.openjdk.java.net/~gbierman/jep409/latest/ > > Comments welcome! > > Thanks, > Gavin -------------- next part -------------- An HTML attachment was scrubbed... URL: From john.r.rose at oracle.com Fri Apr 23 19:27:40 2021 From: john.r.rose at oracle.com (John Rose) Date: Fri, 23 Apr 2021 19:27:40 +0000 Subject: Switch labels (null again), some tweaking In-Reply-To: <9ca67b52-70a1-b0a3-7da0-da96fa5d80ab@oracle.com> References: <9ca67b52-70a1-b0a3-7da0-da96fa5d80ab@oracle.com> Message-ID: <7B2A199C-AC63-4901-8EBF-3102F9CD3BFF@oracle.com> On Apr 23, 2021, at 8:38 AM, Brian Goetz wrote: > > ...The fact is that the natural interpretation of a total type pattern is that it matches null. People don't like this? This person does. As you point out, this position enables composition of patterns. And composition is the more important goal, compared to preservation of intuitions about edge cases. > > So, given that a total type pattern matches null, but legacy switches reject null... > > The treatment of the `null` label solves a few problems: > > - It lets people who want to treat null specially in switch do so, without having to do so outside the switch. Re. composition, it lets the switch be refactored into smaller or larger switches while still managing its own business. No need to bring in extra `if` statements. > - It lets us combine null handling with other things (case null, default:), and plays nicely when those other things have bindings (case null, String s:). > - It provides a visual cue to the reader that this switch is nullable. > > It is this last one that I think we may have over-rotated on. I?m glad you are bringing this up. > In the treatment we've been discussing, we said: > > - switch always throws on null, unless there's a null label > > Now, this is clearly appealing from a "how do I know if a switch throws NPE or not" perspective, so its understandable why this seemed a clever hack. I agree with this goal, as a secondary goal. But the hack is not clever if it harms a more important goal. > ...It might be better to rip the band-aid off, and admit how patterns work. (Because that?s the place Java will be in the long run.) > Here's an example of the kind of mistake that this treatment encourages. Good example. > ...The first switch does the right thing; the second will NPE on Foo(null). And by insulating people from the real behavior of type patterns, it will be even more surprising when this happens. It?s possible to tweak the code to work around the problem by adding `null,` on the last case. And an IDE could do this. But it?s a sharp edge that comes from the extra interference of `switch` into pattern semantics, which harms the primary goal of composition. > Now, let's look back at the alternative, where we keep the flexibility of the null label, but treat patterns as meaning what they mean, and letting switch decide to throw based on whether there is a nullable pattern or not. So a switch with a total type pattern -- that is, `var x` or `Object x` -- will accept null, and thread it into the total case (which also must be the last case.) To me this is the material point, and has been all along: There is never a need to perform an O(N) visual scan of a switch to see if it accepts nulls, since the users simply have to inspect the final (and perhaps initial) case of the the switch. Good style will avoid puzzlers such as final cases which are difficult to classify (total vs. partial). The language does not have to carry the burden of enforcing full annotation. A second point: In the present design, and especially with the cleanup you are proposing, I think there are several possible (optionally selectable) styles of null processing in switches. (Reading later I see you already brought up most of these points.) Style A1 is implicit null acceptance. Pattern totality determines null acceptance, end of story. Style A2 is implicit null rejection, with markup of non-totality using `default`. That is, a coder could choose, as a matter of clarity, to call out partial final cases by adding `default: break;` after them. (E-switches don?t need A2.) Style B1 is explicit null acceptance. If null is allowed by the switch, then the token `null` must be present, either at the head of the switch or just before the final total pattern. Style B2 adds explicit null rejection, by adding something like `case null: throw ?`. That?s probably too much noise in most cases. The current draft mandates explicit null acceptance. Brian is suggesting that we allow the other style too. I think it?s a good suggestion. (An extra option would be to allow some bespoke way to annotate totality and/or non-totality of the final case. That interacts with things like enums and sealed classes, so I?ll just mention it and move on.) I think that IDEs can help users pick the style that is correct for them. Bottom line: Trust the users to choose how explicit to be with nulls. More importantly, trust them with compositional notations. > Who is this going to burn, that is not going to be burned by the existing switch behavior anyway? I think very, very few people. To get burned, a lot of things have to come together. People are used to saying `default`; those that continue to are not going to get burned. (Style A2.) > People are generally in agreement that `var x` should be total; people who use that are not going to get burned. Switches today NPE eagerly on null, so having a null flow into code that doesn't expect it will result in ... the same NPE. > > And, people who want to be explicit can say: > > case null, Object o: (Style B1.) > and it will work -- and maybe even IntelliJ will hint them "hey, did you know this null is redundant?" And then learning will happen! (Now I see I should have read ahead. Oops.) ? John From brian.goetz at oracle.com Fri Apr 23 20:10:10 2021 From: brian.goetz at oracle.com (Brian Goetz) Date: Fri, 23 Apr 2021 16:10:10 -0400 Subject: Switch labels (null again), some tweaking In-Reply-To: <7B2A199C-AC63-4901-8EBF-3102F9CD3BFF@oracle.com> References: <9ca67b52-70a1-b0a3-7da0-da96fa5d80ab@oracle.com> <7B2A199C-AC63-4901-8EBF-3102F9CD3BFF@oracle.com> Message-ID: <8fa97b03-a2da-786d-8bf0-801265bfc377@oracle.com> >> Now, let's look back at the alternative, where we keep the flexibility of the null label, but treat patterns as meaning what they mean, and letting switch decide to throw based on whether there is a nullable pattern or not. So a switch with a total type pattern -- that is, `var x` or `Object x` -- will accept null, and thread it into the total case (which also must be the last case.) > To me this is the material point, and has been all along: > There is never a need to perform an O(N) visual scan of > a switch to see if it accepts nulls, since the users simply > have to inspect the final (and perhaps initial) case of the > the switch. Good style will avoid puzzlers such as final > cases which are difficult to classify (total vs. partial). > The language does not have to carry the burden of > enforcing full annotation. I think the concern on the part of the null-fearers is not so much the O(n) scan (though that's a concern), as much as the subtle difference between `case Object o` and `default`.? They are almost identical, except in null.? No one likes having to carry that distinction around in their head. The essential point here is, though: that subtle distinction has to live somewhere; we don't get to banish it, we just get to try to put it where it does the least damage.? And the place that seems least damaging in the short run, is likely more damaging in the long run. (I am starting to think I should rethink Serialization's position as my "most regretted feature", given the degree to which null distorts every language evolution discussion....) -------------- next part -------------- An HTML attachment was scrubbed... URL: From john.r.rose at oracle.com Fri Apr 23 20:19:17 2021 From: john.r.rose at oracle.com (John Rose) Date: Fri, 23 Apr 2021 20:19:17 +0000 Subject: [sealed-types] Draft Spec for JEP 409 Sealed Types (Final) In-Reply-To: References: Message-ID: <228BFF00-9BEE-4403-894E-9D41DA9AF8B4@oracle.com> On Apr 20, 2021, at 2:00 AM, Gavin Bierman wrote: > > > http://cr.openjdk.java.net/~gbierman/jep409/latest/ Out of curiosity I compared the two large new sections on permits, 8.1.4 and 9.1.4, perhaps to find unintentional divergences. Here are the differences I found FYI, aside from than the required differences arising from section numbering and the parallel terminology of interfaces and lists. > The optional permits clause in a [C/I] declaration {specifies, lists} the [C/I]s > It is a compile-time error if {the same class, a class or interface} is {specified, named} more than once in a {, single} permits clause > The permitted direct sub[C/I]s of a sealed [C/I] are the [C/I]s listed by its permits clause, or, if [C/I] lacks a permits clause, each {, top level or member} [C/I] declared in the same compilation unit as [C/I] (7.3) which has a canonical name (6.7) This last one is especially odd, since ?top level or member? seems redundant when ?has a canonical name? is specified. The latter condition is in both places, and is more pertinent. Also, this comment was in the class section, but there was no parallel comment in the interface section: > This means that if a sealed class C lacks a permits clause one will be inferred that contains all the top-level and member classes in the same compilation unit that list C as their direct superclass. The requirement of having a canonical name means that no local classes or anonymous classes will be considered. Perhaps you added the ?canonical name? and did not replace all of the predecessor language about ?top level or member?? (As expected, I found no substantive differences.) ? John From forax at univ-mlv.fr Fri Apr 23 20:25:32 2021 From: forax at univ-mlv.fr (Remi Forax) Date: Fri, 23 Apr 2021 22:25:32 +0200 (CEST) Subject: Switch labels (null again), some tweaking In-Reply-To: <7B2A199C-AC63-4901-8EBF-3102F9CD3BFF@oracle.com> References: <9ca67b52-70a1-b0a3-7da0-da96fa5d80ab@oracle.com> <7B2A199C-AC63-4901-8EBF-3102F9CD3BFF@oracle.com> Message-ID: <367476914.1448374.1619209532172.JavaMail.zimbra@u-pem.fr> ----- Mail original ----- > De: "John Rose" > ?: "Brian Goetz" > Cc: "amber-spec-experts" > Envoy?: Vendredi 23 Avril 2021 21:27:40 > Objet: Re: Switch labels (null again), some tweaking > On Apr 23, 2021, at 8:38 AM, Brian Goetz wrote: >> >> ...The fact is that the natural interpretation of a total type pattern is that >> it matches null. People don't like this? > > This person does. As you point out, this position > enables composition of patterns. And composition > is the more important goal, compared to preservation > of intuitions about edge cases. I agree composition is more important, a total pattern should match null because semantically a total pattern is equivalent to "else" and not "if instanceof". [...] >> Now, let's look back at the alternative, where we keep the flexibility of the >> null label, but treat patterns as meaning what they mean, and letting switch >> decide to throw based on whether there is a nullable pattern or not. So a >> switch with a total type pattern -- that is, `var x` or `Object x` -- will >> accept null, and thread it into the total case (which also must be the last >> case.) > > To me this is the material point, and has been all along: > There is never a need to perform an O(N) visual scan of > a switch to see if it accepts nulls, since the users simply > have to inspect the final (and perhaps initial) case of the > the switch. Good style will avoid puzzlers such as final > cases which are difficult to classify (total vs. partial). > The language does not have to carry the burden of > enforcing full annotation. > > A second point: In the present design, and especially > with the cleanup you are proposing, I think there are > several possible (optionally selectable) styles of > null processing in switches. > > (Reading later I see you already brought up most > of these points.) > > Style A1 is implicit null acceptance. Pattern > totality determines null acceptance, end of story. > Style A2 is implicit null rejection, with markup > of non-totality using `default`. That is, a coder > could choose, as a matter of clarity, to call out > partial final cases by adding `default: break;` > after them. (E-switches don?t need A2.) > > Style B1 is explicit null acceptance. If null > is allowed by the switch, then the token > `null` must be present, either at the head > of the switch or just before the final total > pattern. Style B2 adds explicit null rejection, > by adding something like `case null: throw ?`. > That?s probably too much noise in most cases. > > The current draft mandates explicit null > acceptance. Brian is suggesting that we > allow the other style too. I think it?s a > good suggestion. > > (An extra option would be to allow some > bespoke way to annotate totality and/or > non-totality of the final case. If we have a change to do, i'm not sure, i would prefer that option. Make clear that the last pattern is total so it matches null. > That interacts with things like enums and sealed classes, > so I?ll just mention it and move on.) ???, in those cases, no total pattern is needed, i don't see the interaction. > > I think that IDEs can help users pick > the style that is correct for them. > > Bottom line: Trust the users to choose how > explicit to be with nulls. More importantly, > trust them with compositional notations. That's a solution, we do nothing and trust users. The other solution is to force users to explicit say that the pattern is total, by example by asking to use 'var o' instead of 'Object o', but we already discuss that and only me and Tagir were agreeing that it is a good idea. [...] > > ? John R?mi From brian.goetz at oracle.com Fri Apr 23 20:38:41 2021 From: brian.goetz at oracle.com (Brian Goetz) Date: Fri, 23 Apr 2021 16:38:41 -0400 Subject: [External] : Re: Switch labels (null again), some tweaking In-Reply-To: <367476914.1448374.1619209532172.JavaMail.zimbra@u-pem.fr> References: <9ca67b52-70a1-b0a3-7da0-da96fa5d80ab@oracle.com> <7B2A199C-AC63-4901-8EBF-3102F9CD3BFF@oracle.com> <367476914.1448374.1619209532172.JavaMail.zimbra@u-pem.fr> Message-ID: On 4/23/2021 4:25 PM, Remi Forax wrote: >> Bottom line: Trust the users to choose how >> explicit to be with nulls. More importantly, >> trust them with compositional notations. > That's a solution, we do nothing and trust users. > The other solution is to force users to explicit say that the pattern is total, by example by asking to use 'var o' instead of 'Object o', > but we already discuss that and only me and Tagir were agreeing that it is a good idea. Yes, this is what I meant by "move the distinction around."? We could say that `var x` and `Object x` are different patterns; this is a sharp edge in one place (blows up the notion that `var` is just type inference.)? We could say that `default` and `Object x` are different patterns (which is what I'm suggesting), which puts the sharp edge somewhere else -- it means that people's notion of totality is polluted by historical chance.? We could have different patterns for Object! and Object?, which is appealing in a "say what you mean" way, but which forces users to think about a corner case all the time. Reasonable people can differ about which of these (or other) approach is best for Java, but it is not reasonable to believe that we can hide this distinction. -------------- next part -------------- An HTML attachment was scrubbed... URL: From john.r.rose at oracle.com Fri Apr 23 20:39:45 2021 From: john.r.rose at oracle.com (John Rose) Date: Fri, 23 Apr 2021 20:39:45 +0000 Subject: Switch labels (null again), some tweaking In-Reply-To: <8fa97b03-a2da-786d-8bf0-801265bfc377@oracle.com> References: <9ca67b52-70a1-b0a3-7da0-da96fa5d80ab@oracle.com> <7B2A199C-AC63-4901-8EBF-3102F9CD3BFF@oracle.com> <8fa97b03-a2da-786d-8bf0-801265bfc377@oracle.com> Message-ID: <132A8B5E-544D-49F9-9BA5-F59E5A852C50@oracle.com> On Apr 23, 2021, at 1:10 PM, Brian Goetz > wrote: I think the concern on the part of the null-fearers is not so much the O(n) scan (though that's a concern), as much as the subtle difference between `case Object o` and `default`. They are almost identical, except in null. No one likes having to carry that distinction around in their head. I mildly disagree with this last point: I think that distinction will cause mild pain to learn, and zero point zero zero pain to carry around for the rest of one?s career. I can feel the rule growing in my own head: ?default means old-school null rejection, a null case label means null acceptance, if both are present the null case label of course wins.? Could this explicit null-ing be MaKing the New nUlls Stand Out? ? John -------------- next part -------------- An HTML attachment was scrubbed... URL: From john.r.rose at oracle.com Fri Apr 23 20:42:08 2021 From: john.r.rose at oracle.com (John Rose) Date: Fri, 23 Apr 2021 20:42:08 +0000 Subject: [External] : Re: Switch labels (null again), some tweaking In-Reply-To: References: <9ca67b52-70a1-b0a3-7da0-da96fa5d80ab@oracle.com> <7B2A199C-AC63-4901-8EBF-3102F9CD3BFF@oracle.com> <367476914.1448374.1619209532172.JavaMail.zimbra@u-pem.fr> Message-ID: <12C175DD-0FB3-4556-A53E-773891F1E555@oracle.com> On Apr 23, 2021, at 1:38 PM, Brian Goetz > wrote: On 4/23/2021 4:25 PM, Remi Forax wrote: Bottom line: Trust the users to choose how explicit to be with nulls. More importantly, trust them with compositional notations. That's a solution, we do nothing and trust users. The other solution is to force users to explicit say that the pattern is total, by example by asking to use 'var o' instead of 'Object o', but we already discuss that and only me and Tagir were agreeing that it is a good idea. Yes, this is what I meant by "move the distinction around." We could say that `var x` and `Object x` are different patterns; this is a sharp edge in one place (blows up the notion that `var` is just type inference.) We could say that `default` and `Object x` are different patterns (which is what I'm suggesting), which puts the sharp edge somewhere else -- it means that people's notion of totality is polluted by historical chance. We could have different patterns for Object! and Object?, which is appealing in a "say what you mean" way, but which forces users to think about a corner case all the time. Reasonable people can differ about which of these (or other) approach is best for Java, but it is not reasonable to believe that we can hide this distinction. Making ?default? the oddity feels like the safest move, on several grounds: - it?s localized to switch structure (and we can localize further to the end of the switch) - it is visually clear (a different token from ?case?) - it carries a connotation of ?old school? (fancy case labels are the future) -------------- next part -------------- An HTML attachment was scrubbed... URL: From forax at univ-mlv.fr Fri Apr 23 20:44:41 2021 From: forax at univ-mlv.fr (forax at univ-mlv.fr) Date: Fri, 23 Apr 2021 22:44:41 +0200 (CEST) Subject: [External] : Re: Switch labels (null again), some tweaking In-Reply-To: References: <9ca67b52-70a1-b0a3-7da0-da96fa5d80ab@oracle.com> <7B2A199C-AC63-4901-8EBF-3102F9CD3BFF@oracle.com> <367476914.1448374.1619209532172.JavaMail.zimbra@u-pem.fr> Message-ID: <287002393.1453331.1619210681405.JavaMail.zimbra@u-pem.fr> > De: "Brian Goetz" > ?: "Remi Forax" , "John Rose" > Cc: "amber-spec-experts" > Envoy?: Vendredi 23 Avril 2021 22:38:41 > Objet: Re: [External] : Re: Switch labels (null again), some tweaking > On 4/23/2021 4:25 PM, Remi Forax wrote: >>> Bottom line: Trust the users to choose how >>> explicit to be with nulls. More importantly, >>> trust them with compositional notations. >> That's a solution, we do nothing and trust users. >> The other solution is to force users to explicit say that the pattern is total, >> by example by asking to use 'var o' instead of 'Object o', >> but we already discuss that and only me and Tagir were agreeing that it is a >> good idea. > Yes, this is what I meant by "move the distinction around." We could say that > `var x` and `Object x` are different patterns; this is a sharp edge in one > place (blows up the notion that `var` is just type inference.) We could say > that `default` and `Object x` are different patterns (which is what I'm > suggesting), which puts the sharp edge somewhere else -- it means that people's > notion of totality is polluted by historical chance. We could have different > patterns for Object! and Object?, which is appealing in a "say what you mean" > way, but which forces users to think about a corner case all the time. For the record, I was proposing to disallow `Object o` as a total pattern (as we disallow instanceof Object o) instead of making `var x` and `Object x` two different patterns, well, it also can be seen as two different patterns with `Object o` being an invalid one. > Reasonable people can differ about which of these (or other) approach is best > for Java, but it is not reasonable to believe that we can hide this > distinction. Yes ! R?mi -------------- next part -------------- An HTML attachment was scrubbed... URL: From forax at univ-mlv.fr Sun Apr 25 11:39:35 2021 From: forax at univ-mlv.fr (Remi Forax) Date: Sun, 25 Apr 2021 13:39:35 +0200 (CEST) Subject: Switch labels (null again), some tweaking In-Reply-To: <9ca67b52-70a1-b0a3-7da0-da96fa5d80ab@oracle.com> References: <9ca67b52-70a1-b0a3-7da0-da96fa5d80ab@oracle.com> Message-ID: <1904623133.1822034.1619350775381.JavaMail.zimbra@u-pem.fr> I just want to add that there is another possible semantics than having case null + total pattern as null acceptable pattern. We have based the semantics of pattern matching to the semantics of the cascade of if instanceof with an else at the end. There is another one, method calls [1], a switch like this switch(o) { case String s: case Object o: } can be seen as equivalent to the methods void m(String s) { ... void m(Object o) { ... with the switch being equivalent to a call to the method m. This semantics is equivalent to the cascade of instanceof apart in one scenario, when the value is null. A call to m(null) invokes m(String s), not m(Object). You may think that the method call semantics is not a good one because it can answer that no method are applicable or no method are more specific than the other, but those cases are not possible if the switch is total ... apart with null. If the value is null and we have a switch like this switch(o) { case String s: case Integer i: case Object o: } if we use the method call semantics, either "String s" or "Integer i" can be called, and given that none is more specific than the other. We can deviate from the original method call semantics by saying, let's take the first match, but in that case, it's just a different semantics, not an existing Java semantics. I don't like this semantics, because it will be surprising for anyone that null will be captured by the first case. I prefer null to be trapped by the total pattern at the end. Sadly, it means that there is no *obvious* semantics for null and that whatever we choose, it will not what some people expect. R?mi [1] https://docs.oracle.com/javase/specs/jls/se16/html/jls-15.html#jls-15.12 > De: "Brian Goetz" > ?: "amber-spec-experts" > Envoy?: Vendredi 23 Avril 2021 17:38:42 > Objet: Re: Switch labels (null again), some tweaking > Gavin has a spec that captures most of what we've talked about here, which > should be posted soon. But I want to revisit one thing, because I think we may > have swung a little too eagerly at a bad pitch. > There is a distinction between the _semantics of a given pattern_ and _what a > given construct might do with that pattern_. The construct (instanceof, switch) > gets first crack at having an opinion, and then may choose to evaluate whether > the pattern matches something. The place where we have been most tempted to use > this flexibility is with "null", because null ruins everything. > We made a decision to lump pattern matching in with `instanceof` because it > seemed silly to have two almost identical but subtly different constructs for > "dynamic type test" and "pattern match" (given that you can do dynamic type > tests with patterns.) We knew that this would have some uncomfortable > consequences, and what we have tentatively decided to do is outlaw total > patterns in instanceof, so that users are not confronted with the subtle > difference between `x instanceof Object` and `x instanceof Object o`. This may > not be a totally satisfying answer, and we left some room to adjust this, but > its where we are. > The fact is that the natural interpretation of a total type pattern is that it > matches null. People don't like this, partially because it goes against the > intuition that's been built up by instanceof and switch. (If we have to, I'm > willing to lay out the argument again, but after having been through it umpteen > times, I don't see any of the alternatives as being better.) > So, given that a total type pattern matches null, but legacy switches reject > null... > The treatment of the `null` label solves a few problems: > - It lets people who want to treat null specially in switch do so, without > having to do so outside the switch. > - It lets us combine null handling with other things (case null, default:), and > plays nicely when those other things have bindings (case null, String s:). > - It provides a visual cue to the reader that this switch is nullable. > It is this last one that I think we may have over-rotated on. In the treatment > we've been discussing, we said: > - switch always throws on null, unless there's a null label > Now, this is clearly appealing from a "how do I know if a switch throws NPE or > not" perspective, so its understandable why this seemed a clever hack. But > reading the responses again: > Tagir: > > I support making case null the only null-friendly pattern. > Maurizio: > > there's no subtle type dependency analysis which determines the fate of null > it seems that people wanted to read way more into this than there was; they > wanted this to be a statement about patterns, not about switch. I think this is > yet another example of the "I hate null so much I'm willing to take anything to > make it (appear to) go away" biases we all have." Only Remi seemed to have > recognized this for the blatant trick it is -- we're hiding the null-accepting > behavior of total patterns until people encounter them with nested patterns, > where it will be less uncomfortable. > To be clear: this is *not* making `null` the only null-friendly pattern. But > these responses make me worry that, if the experts can't tell the difference, > then no user will be able to tell the difference, and that this is just kicking > the confusion down the road. It might be better to rip the band-aid off, and > admit how patterns work. > Here's an example of the kind of mistake that this treatment encourages. If we > have: > switch (x) { > case Foo(String a): A > case Foo(Integer a): B > case Foo(Object a): C > } > and we want to refactor to > switch (x) { > case Foo(var a): > switch(a) { > case String a: A > case Integer a: B > case Object a: C > } > } > we've made a mistake. The first switch does the right thing; the second will NPE > on Foo(null). And by insulating people from the real behavior of type patterns, > it will be even more surprising when this happens. > Now, let's look back at the alternative, where we keep the flexibility of the > null label, but treat patterns as meaning what they mean, and letting switch > decide to throw based on whether there is a nullable pattern or not. So a > switch with a total type pattern -- that is, `var x` or `Object x` -- will > accept null, and thread it into the total case (which also must be the last > case.) > Who is this going to burn, that is not going to be burned by the existing switch > behavior anyway? I think very, very few people. To get burned, a lot of things > have to come together. People are used to saying `default`; those that continue > to are not going to get burned. People are generally in agreement that `var x` > should be total; people who use that are not going to get burned. Switches > today NPE eagerly on null, so having a null flow into code that doesn't expect > it will result in ... the same NPE. > And, people who want to be explicit can say: > case null, Object o: > and it will work -- and maybe even IntelliJ will hint them "hey, did you know > this null is redundant?" And then learning will happen! > So, I think the "a switch only accepts null if the letters n-u-l-l are present", > while a comforting move in the short term, buys us relatively little, and dulls > our pain receptors which in turn makes it take way longer to learn how patterns > really work. I think we should go back to: > - A switch accepts null if (a) one of the case labels is `null` or (b) the > switch has a total pattern (which must always be the last case.) > On 3/12/2021 9:12 AM, Brian Goetz wrote: >> The JEP has some examples of how the `null` case label can combine with others. >> But I would like to propose a more general way to describe what's going on. >> This doesn't change the proposed language (much), as much as >> describing/specifying it in a more general way. >> We have the following kinds of switch labels: >> case >> case null >> case >> default >> The question is, which can be combined with each other into a single case, such >> as: >> case 3, null, 5: >> This question is related to, which can fall into each other: >> case 3: >> case null: >> case 5: >> We can say that certain labels are compatible with certain others, and ones that >> are compatible can be combined / are candidates for fallthrough, by defining a >> compatibility predicate: >> - All case labels are compatible with each other; >> - The `null` label is compatible with labels; >> - The `null` label is compatible with `default`; >> - The `null` label is compatible with (explicit) type patterns: >> (There is already a check that each label is applicable to the type of the >> target.) >> Then we say: you can combine N case labels as long as they are all compatible >> with each other. Combination includes both comma-separated lists in one case, >> as well as when one case is _reachable_ from another (fall through.) And the >> two can be combined: >> case 3, 4: // fall through >> case null: >> So the following are allowed: >> case 1, 2, 3, null: >> case null, 1, 2, 3: >> case null, Object o: >> case Object o, null: >> case null, default: // special syntax rule for combining default >> The following special rules apply: >> - `default` can be used as a case label when combined with compatible case >> labels (see last example above); >> - When `null` combines with a type pattern, the binding variable of the type >> pattern can bind null. >> The semantics outlined in Gavin's JEP are unchanged; this is just a new and less >> fussy way to describe the behavior of the null label / specify the interaction >> with fallthrough. -------------- next part -------------- An HTML attachment was scrubbed... URL: From maurizio.cimadamore at oracle.com Tue Apr 27 14:23:20 2021 From: maurizio.cimadamore at oracle.com (Maurizio Cimadamore) Date: Tue, 27 Apr 2021 15:23:20 +0100 Subject: Switch labels (null again), some tweaking In-Reply-To: <9ca67b52-70a1-b0a3-7da0-da96fa5d80ab@oracle.com> References: <9ca67b52-70a1-b0a3-7da0-da96fa5d80ab@oracle.com> Message-ID: On 23/04/2021 16:38, Brian Goetz wrote: > So, I think the "a switch only accepts null if the letters n-u-l-l are > present", while a comforting move in the short term, buys us > relatively little, and dulls our pain receptors which in turn makes it > take way longer to learn how patterns really work.? I think we should > go back to: > > ?- A switch accepts null if (a) one of the case labels is `null` or > (b) the switch has a total pattern (which must always be the last case.) The proposal seems ok; it does nothing for the problem I'm concerned about (e.g. type of the target expression changing and influencing the totality analysis at a distance) - but that was not address by the previous proposal either (as you say in your email, admittedly, I was reading too much into your proposal). Stepping back - my general feeling on this topic is that the audience in this mailing list have a very intimate knowledge of what a "total pattern" is, to the point that they are comfortable building on top of this definition to e.g. define null behavior of patterns. I'm a little afraid that the finer detail of totality might be lost on the average Joe developer: totality is a much more slippery concept than it seems. Sure, there is one obvious way to make your pattern total: if the target expression has type E, then add a type test pattern whose type is also E. That seems easy enough. Except that, the type of E will not always be that manifest in user code (e.g. might be the result of what javac happens to infer on Tuesdays). And, if you mix this with sealed classes, it might be possible for a switch to go from total to non-total, as new permitted subtypes are added to a sealed hierarchy. These might all be all corner cases - but I think it's this complexity which contributes to my "isn't this all too subtle?" feeling. Obviously I'm well aware that nearly every path has been explored, and no silver bullet seems to be laying around, so... this might just be the best we can offer, and that's ok. Cheers Maurizio -------------- next part -------------- An HTML attachment was scrubbed... URL: From brian.goetz at oracle.com Tue Apr 27 16:39:40 2021 From: brian.goetz at oracle.com (Brian Goetz) Date: Tue, 27 Apr 2021 12:39:40 -0400 Subject: Switch labels (null again), some tweaking In-Reply-To: References: <9ca67b52-70a1-b0a3-7da0-da96fa5d80ab@oracle.com> Message-ID: You've put your finger on the fundamental stewardship challenge here.? If we were never going to go any farther than "deconstruction patterns in switch", then we would surely do a "worse is better" hack which separates nullity from totality.? But, if we want deconstruction patterns (or Optional.of(var x), or array patterns, or collection patterns, or pattern assignment, or ...), we have to confront _nesting_, and nesting puts the connection between totality and target type and nullity right in your face. But, Joe Java has not yet thought very much about nesting, so the answer that you get to when you work out all the details is ... counterintuitive.?? As a result, we've been bombarded with (at varying levels of constructiveness) attempts to hide this complexity.? But at root, we have a choice: we can solve for the whole problem, or we can solve for the easy problems and make the hard ones even harder.? This is not an easy, or obvious, choice. My claim is that to make pattern matching successful in Java, where we have to spend our "get users to think about something new" budget is totality.? And totality is relative to "what are you total on." Where I think we need to shore up the story is in "how do I turn on exhaustiveness checking for statement switches."? We already have that for expression switches, and several of the holes you are worried that users will fall into come from the fact that the compiler is sometimes, but not always, type checking switches for exhaustiveness. We've been struggling with whether there are "pattern switches" and "legacy switches", or whether we've fully generalized switch.? If the former is unavoidable, it might be possible to enforce totality checking on all but the legacy constant switches.? After all, totalizing an intentionally partial switch is simple: add a `default: break` clause. > Stepping back - my general feeling on this topic is that the audience > in this mailing list have a very intimate knowledge of what a "total > pattern" is, to the point that they are comfortable building on top of > this definition to e.g. define null behavior of patterns. I'm a little > afraid that the finer detail of totality might be lost on the average > Joe developer: totality is a much more slippery concept than it seems. > Sure, there is one obvious way to make your pattern total: if the > target expression has type E, then add a type test pattern whose type > is also E. That seems easy enough. Except that, the type of E will not > always be that manifest in user code (e.g. might be the result of what > javac happens to infer on Tuesdays). And, if you mix this with sealed > classes, it might be possible for a switch to go from total to > non-total, as new permitted subtypes are added to a sealed hierarchy. > These might all be all corner cases - but I think it's this complexity > which contributes to my "isn't this all too subtle?" feeling. > > Obviously I'm well aware that nearly every path has been explored, and > no silver bullet seems to be laying around, so... this might just be > the best we can offer, and that's ok. > > Cheers > Maurizio > > > -------------- next part -------------- An HTML attachment was scrubbed... URL: From brian.goetz at oracle.com Tue Apr 27 17:11:18 2021 From: brian.goetz at oracle.com (Brian Goetz) Date: Tue, 27 Apr 2021 13:11:18 -0400 Subject: Make all switches exhaustive? Message-ID: <5fee06c7-52d3-567e-5222-677a9c81216d@oracle.com> Maurizio's comment got me thinking about a possible way to eliminate the sharp edge of "sometimes switches are type-checked for exhaustiveness, and sometimes not." Historically, there is no requirement that switches be exhaustive; legacy switches without a default are like unbalanced ifs.? (Notably, though, the DA rules do distinguish between switches with and without a default, and switches implicitly reject any null remainder -- so we still do have a solid base to build on here.) When we did expression switches, we said "expression switches must be total."? This was a forced move if we wanted to reuse "switch", because expressions must be total and compatibility backed us into letting statement switches be partial. Unfortunately, this leaves statement switches with less type checking than expression switches, since there is no way to even opt into exhaustiveness checking. As switch gets more powerful (e.g., switching over sealed types with patterns), this lack of exhaustiveness checking becomes a bigger deal.? We'd been imagining that we'd have to "patch" switch to allow opting into exhaustiveness checking, but maybe there's another way. As we are about to dramatically extend the power of switch (by permitting pattern labels, and allowing switches on other types than integers, boxes, strings, and enums), what we could do is: ?- Erroring on non-exhaustive non-legacy statement switches now; ?- Warning on non-exhaustive legacy statement switches now; ?- Later upgrading these warnings to errors, after a period of time where people can update their code. The world we end up in in the long run is: all switches are exhaustive, with no new language surface for managing exhaustiveness. Legacy switches are those whose operand type is one of the "classic" types and all labels are constant labels or "default". For a switch that is deliberately non-exhaustive, all the user has to do to capture this (and shut up the compiler) is: ??? default: break; which is not very intrusive, and arguably makes the code more readable anyway.? Users will see a speed bump when upgrading to pattern switches (clippy will tell them "I see you're writing a pattern switch, don't forget to end it with default:break") which presumably they will quickly internalize. (How's that for teaching an old dog new tricks?) -------------- next part -------------- An HTML attachment was scrubbed... URL: From forax at univ-mlv.fr Tue Apr 27 17:47:27 2021 From: forax at univ-mlv.fr (Remi Forax) Date: Tue, 27 Apr 2021 19:47:27 +0200 (CEST) Subject: Make all switches exhaustive? In-Reply-To: <5fee06c7-52d3-567e-5222-677a9c81216d@oracle.com> References: <5fee06c7-52d3-567e-5222-677a9c81216d@oracle.com> Message-ID: <787667654.1564636.1619545647529.JavaMail.zimbra@u-pem.fr> > De: "Brian Goetz" > ?: "amber-spec-experts" > Envoy?: Mardi 27 Avril 2021 19:11:18 > Objet: Make all switches exhaustive? > Maurizio's comment got me thinking about a possible way to eliminate the sharp > edge of "sometimes switches are type-checked for exhaustiveness, and sometimes > not." > Historically, there is no requirement that switches be exhaustive; legacy > switches without a default are like unbalanced ifs. (Notably, though, the DA > rules do distinguish between switches with and without a default, and switches > implicitly reject any null remainder -- so we still do have a solid base to > build on here.) > When we did expression switches, we said "expression switches must be total." > This was a forced move if we wanted to reuse "switch", because expressions must > be total and compatibility backed us into letting statement switches be > partial. Unfortunately, this leaves statement switches with less type checking > than expression switches, since there is no way to even opt into exhaustiveness > checking. > As switch gets more powerful (e.g., switching over sealed types with patterns), > this lack of exhaustiveness checking becomes a bigger deal. We'd been imagining > that we'd have to "patch" switch to allow opting into exhaustiveness checking, > but maybe there's another way. > As we are about to dramatically extend the power of switch (by permitting > pattern labels, and allowing switches on other types than integers, boxes, > strings, and enums), what we could do is: > - Erroring on non-exhaustive non-legacy statement switches now; > - Warning on non-exhaustive legacy statement switches now; > - Later upgrading these warnings to errors, after a period of time where people > can update their code. > The world we end up in in the long run is: all switches are exhaustive, with no > new language surface for managing exhaustiveness. This will not solve the problem of Maurizio for sub-patterns but i like it. We don't have to later transform the warning to an error, keeping it as a warning can be annoying enough to force people to change their code (or ALT-ENTER + CTRL+1 ont it). > Legacy switches are those whose operand type is one of the "classic" types and > all labels are constant labels or "default". > For a switch that is deliberately non-exhaustive, all the user has to do to > capture this (and shut up the compiler) is: > default: break; "default:" should be enough no ? > which is not very intrusive, and arguably makes the code more readable anyway. > Users will see a speed bump when upgrading to pattern switches (clippy will > tell them "I see you're writing a pattern switch, don't forget to end it with > default:break") which presumably they will quickly internalize. > (How's that for teaching an old dog new tricks?) I think we should tackle "default" not being at the end the same way. R?mi -------------- next part -------------- An HTML attachment was scrubbed... URL: From brian.goetz at oracle.com Tue Apr 27 17:55:47 2021 From: brian.goetz at oracle.com (Brian Goetz) Date: Tue, 27 Apr 2021 13:55:47 -0400 Subject: [External] : Re: Make all switches exhaustive? In-Reply-To: <787667654.1564636.1619545647529.JavaMail.zimbra@u-pem.fr> References: <5fee06c7-52d3-567e-5222-677a9c81216d@oracle.com> <787667654.1564636.1619545647529.JavaMail.zimbra@u-pem.fr> Message-ID: <5598be37-ac6e-a638-86bd-6b2be1462913@oracle.com> > > This will not solve the problem of Maurizio for sub-patterns but i > like it. I agree it doesn't *solve* the problem, but it helps somewhat. > We don't have to later transform the warning to an error, keeping it > as a warning can be annoying enough to force people to change their > code (or ALT-ENTER + CTRL+1 ont it). Yes, we can make this decision later. > > > Legacy switches are those whose operand type is one of the > "classic" types and all labels are constant labels or "default". > > For a switch that is deliberately non-exhaustive, all the user has > to do to capture this (and shut up the compiler) is: > > ??? default: break; > > > "default:" should be enough no ? Yes, probably. > > > which is not very intrusive, and arguably makes the code more > readable anyway.? Users will see a speed bump when upgrading to > pattern switches (clippy will tell them "I see you're writing a > pattern switch, don't forget to end it with default:break") which > presumably they will quickly internalize. > > (How's that for teaching an old dog new tricks?) > > > > I think we should tackle "default" not being at the end the same way. > I'd like that; pushing total patterns to the end is pretty helpful. We tried that once, and ran into trouble, though; there were cases we couldn't completely express with that IIRC.? With the latest formulation of `case null, default`, I think the trouble we ran into before goes away, but we might have new trouble doing so. -------------- next part -------------- An HTML attachment was scrubbed... URL: From john.r.rose at oracle.com Tue Apr 27 22:34:20 2021 From: john.r.rose at oracle.com (John Rose) Date: Tue, 27 Apr 2021 22:34:20 +0000 Subject: Switch labels (null again), some tweaking In-Reply-To: References: <9ca67b52-70a1-b0a3-7da0-da96fa5d80ab@oracle.com> Message-ID: <0EFFFE77-9567-4F6C-BF13-FDB8984A1C64@oracle.com> On Apr 27, 2021, at 9:39 AM, Brian Goetz > wrote: Where I think we need to shore up the story is in "how do I turn on exhaustiveness checking for statement switches.? Yes, the non-exhaustiveness of legacy switches turns out to be a hiding place for bugs and misconceptions. I do like your proposal (in the other thread) of turning on non-exhaustiveness by default for as many switches as possible. It would seem to help users think about their code more clearly. And putting ?default: break? at the bottom seems as reasonable ask: Basically, it annotates that switch as non-exhaustive, making the code easier to reason about. In fact, *all* switches become easier to reason about, since the last case is always the fallback. All that said, I hope we don?t run into cases where somebody has written a switch that really looks good w/o a default, and will look worse with one. Surely someone will bring such a breed of switch, eventually, to the Big Pet Show. ? John -------------- next part -------------- An HTML attachment was scrubbed... URL: From forax at univ-mlv.fr Tue Apr 27 23:44:02 2021 From: forax at univ-mlv.fr (Remi Forax) Date: Wed, 28 Apr 2021 01:44:02 +0200 (CEST) Subject: Switch labels (null again), some tweaking In-Reply-To: References: <9ca67b52-70a1-b0a3-7da0-da96fa5d80ab@oracle.com> Message-ID: <996683411.1734289.1619567042958.JavaMail.zimbra@u-pem.fr> > De: "Maurizio Cimadamore" > ?: "Brian Goetz" , "amber-spec-experts" > > Envoy?: Mardi 27 Avril 2021 16:23:20 > Objet: Re: Switch labels (null again), some tweaking > On 23/04/2021 16:38, Brian Goetz wrote: >> So, I think the "a switch only accepts null if the letters n-u-l-l are present", >> while a comforting move in the short term, buys us relatively little, and dulls >> our pain receptors which in turn makes it take way longer to learn how patterns >> really work. I think we should go back to: >> - A switch accepts null if (a) one of the case labels is `null` or (b) the >> switch has a total pattern (which must always be the last case.) > The proposal seems ok; it does nothing for the problem I'm concerned about (e.g. > type of the target expression changing and influencing the totality analysis at > a distance) - but that was not address by the previous proposal either (as you > say in your email, admittedly, I was reading too much into your proposal). At least, if you want a total type, you can make it explicit using case var x which is always total whatever the type switched upon. So you have two tools to avoid influence at distance in the totality analysis, - for a switch where you can enumerate all the values enums, sealed type (boolean should be in that category too IMO, but we did not agree on that), if you list all the possible cases, a total case is not required. - use "var" so the total case is explicit, "case var x", "case Foo(var x)", etc > Stepping back - my general feeling on this topic is that the audience in this > mailing list have a very intimate knowledge of what a "total pattern" is, to > the point that they are comfortable building on top of this definition to e.g. > define null behavior of patterns. I'm a little afraid that the finer detail of > totality might be lost on the average Joe developer: totality is a much more > slippery concept than it seems. Sure, there is one obvious way to make your > pattern total: if the target expression has type E, then add a type test > pattern whose type is also E. That seems easy enough. Except that, the type of > E will not always be that manifest in user code (e.g. might be the result of > what javac happens to infer on Tuesdays). And, if you mix this with sealed > classes, it might be possible for a switch to go from total to non-total, as > new permitted subtypes are added to a sealed hierarchy. These might all be all > corner cases - but I think it's this complexity which contributes to my "isn't > this all too subtle?" feeling. > Obviously I'm well aware that nearly every path has been explored, and no silver > bullet seems to be laying around, so... this might just be the best we can > offer, and that's ok. We tried to have a way to signal that a pattern is total or not, by example, using a keyword, like total or default, but it did not work well when a pattern is partially total like "case Foo(true, var x)" > Cheers > Maurizio R?mi -------------- next part -------------- An HTML attachment was scrubbed... URL: From brian.goetz at oracle.com Wed Apr 28 00:46:13 2021 From: brian.goetz at oracle.com (Brian Goetz) Date: Tue, 27 Apr 2021 20:46:13 -0400 Subject: Switch labels (null again), some tweaking In-Reply-To: <996683411.1734289.1619567042958.JavaMail.zimbra@u-pem.fr> References: <9ca67b52-70a1-b0a3-7da0-da96fa5d80ab@oracle.com> <996683411.1734289.1619567042958.JavaMail.zimbra@u-pem.fr> Message-ID: > - for a switch where you can enumerate all the values enums, sealed > type (boolean should be in that category too IMO, but we did not agree > on that), if you list all the possible cases, a total case is not > required. In fact, when you can enumerate the values/types, it is better to _not_ have a catch-all.? Because then you get better type checking when you only think you listed them all, or a new one is added later. From maurizio.cimadamore at oracle.com Wed Apr 28 09:42:18 2021 From: maurizio.cimadamore at oracle.com (Maurizio Cimadamore) Date: Wed, 28 Apr 2021 10:42:18 +0100 Subject: Switch labels (null again), some tweaking In-Reply-To: <996683411.1734289.1619567042958.JavaMail.zimbra@u-pem.fr> References: <9ca67b52-70a1-b0a3-7da0-da96fa5d80ab@oracle.com> <996683411.1734289.1619567042958.JavaMail.zimbra@u-pem.fr> Message-ID: <969484d1-ce1e-e896-1b9c-675ce79567e9@oracle.com> On 28/04/2021 00:44, Remi Forax wrote: > > > ------------------------------------------------------------------------ > > *De: *"Maurizio Cimadamore" > *?: *"Brian Goetz" , "amber-spec-experts" > > *Envoy?: *Mardi 27 Avril 2021 16:23:20 > *Objet: *Re: Switch labels (null again), some tweaking > > > On 23/04/2021 16:38, Brian Goetz wrote: > > So, I think the "a switch only accepts null if the letters > n-u-l-l are present", while a comforting move in the short > term, buys us relatively little, and dulls our pain receptors > which in turn makes it take way longer to learn how patterns > really work.? I think we should go back to: > > ?- A switch accepts null if (a) one of the case labels is > `null` or (b) the switch has a total pattern (which must > always be the last case.) > > The proposal seems ok; it does nothing for the problem I'm > concerned about (e.g. type of the target expression changing and > influencing the totality analysis at a distance) - but that was > not address by the previous proposal either (as you say in your > email, admittedly, I was reading too much into your proposal). > > > At least, if you want a total type, you can make it explicit using > ? case var x > which is always total whatever the type switched upon. > > So you have two tools to avoid influence at distance in the totality > analysis, > - for a switch where you can enumerate all the values enums, sealed > type (boolean should be in that category too IMO, but we did not agree > on that), if you list all the possible cases, a total case is not > required. Without changes to switch, there is nothing which would break if a "new case" is added (to the enum, or to the sealed hierarchy). So, listing "all possible cases" relies on the assumption that you know what "all cases" are. And right now there's no way to declaratively say: this switch (statement) is gonna cover them all. Brian answer goes a bit in that direction, which is good, for the outermost layer (but the problem still exists somewhat at the nested level). > - use "var" so the total case is explicit,? "case var x", "case > Foo(var x)", etc Sure - this is a good solution. One minor annoying thing is that we started from a position where we said "var is just for inference" and now we're back in a place where, while? var is still, conceptually at least, about inference, developers will also be using it for totality (as inference guarantees that you are always 100% covered). > > Stepping back - my general feeling on this topic is that the > audience in this mailing list have a very intimate knowledge of > what a "total pattern" is, to the point that they are comfortable > building on top of this definition to e.g. define null behavior of > patterns. I'm a little afraid that the finer detail of totality > might be lost on the average Joe developer: totality is a much > more slippery concept than it seems. Sure, there is one obvious > way to make your pattern total: if the target expression has type > E, then add a type test pattern whose type is also E. That seems > easy enough. Except that, the type of E will not always be that > manifest in user code (e.g. might be the result of what javac > happens to infer on Tuesdays). And, if you mix this with sealed > classes, it might be possible for a switch to go from total to > non-total, as new permitted subtypes are added to a sealed > hierarchy. These might all be all corner cases - but I think it's > this complexity which contributes to my "isn't this all too > subtle?" feeling. > > Obviously I'm well aware that nearly every path has been explored, > and no silver bullet seems to be laying around, so... this might > just be the best we can offer, and that's ok. > > > We tried to have a way to signal that a pattern is total or not, by > example, using a keyword, like total or default, but it did not work > well when a pattern is partially total like "case Foo(true, var x)" Yep - I'm aware of the various roads not taken - I'm not suggesting that there's some shiny great solution which is better than what is being proposed; I just wanted to air my concerns about the complexity of the programming model implied in this proposal. In other cases (DA/DU, scoping of pattern variables and, to a certain degree type inference, esp. after Java 8) the rules are mind-blowingly complex, but developers can form an intuitive notion of how these things work, and trust the compiler to do the right thing - e.g. these features, despite their complexity are mostly out of your face, and when the compiler barks, the user will have some idea of what went wrong (whooops, uninitialized final variable here!). Totality analysis is not as complex as some of the features mentioned above - but it is something that users will have to think about in the back of their minds, when reasoning about how their pattern switches work. Maurizio > > > Cheers > Maurizio > > > R?mi > -------------- next part -------------- An HTML attachment was scrubbed... URL: From brian.goetz at oracle.com Wed Apr 28 14:31:59 2021 From: brian.goetz at oracle.com (Brian Goetz) Date: Wed, 28 Apr 2021 10:31:59 -0400 Subject: Switch labels (null again), some tweaking In-Reply-To: <969484d1-ce1e-e896-1b9c-675ce79567e9@oracle.com> References: <9ca67b52-70a1-b0a3-7da0-da96fa5d80ab@oracle.com> <996683411.1734289.1619567042958.JavaMail.zimbra@u-pem.fr> <969484d1-ce1e-e896-1b9c-675ce79567e9@oracle.com> Message-ID: <76bf2826-b987-b606-271e-ad04c30d6509@oracle.com> >> >> At least, if you want a total type, you can make it explicit using >> ? case var x >> which is always total whatever the type switched upon. >> >> So you have two tools to avoid influence at distance in the totality >> analysis, >> - for a switch where you can enumerate all the values enums, sealed >> type (boolean should be in that category too IMO, but we did not >> agree on that), if you list all the possible cases, a total case is >> not required. > > Without changes to switch, there is nothing which would break if a > "new case" is added (to the enum, or to the sealed hierarchy). So, > listing "all possible cases" relies on the assumption that you know > what "all cases" are. And right now there's no way to declaratively > say: this switch (statement) is gonna cover them all. Brian answer > goes a bit in that direction, which is good, for the outermost layer > (but the problem still exists somewhat at the nested level). > It depends on how much sealing there is.? Suppose we have sealed types Container = Box|Bag and Lunch=Soup|Sandwich.? If we are switching on a Container: ??? switch (lunch) { ??????? case Box(Soup s): ??????? case Box(Sandwich s): ??????? case Bag(Soup s): ??????? case Bag(Sandwich s): ??? } This switch is exhaustive because: ?- { Soup, Sandwich } is total on on Lunch (with remainder null) ?- therefore { Box(Soup), Box(Sandwich) } is total on Box (with remainder null, Box(null) } ?- similarly { Bag(Soup), Bag(Sandwich) } is total on Bag (with remainder null, Bag(null) } ?- { Box, Bag } is total on Container (with remainder null) ?- therefore the cases in our switch are exhaustive on Container If Lunch were not sealed, the compiler would complain, and we'd need extra cases to get totality on Box and on Bag, which is needed to get totality on Container: ??? switch (lunch) { ??????? case Box(Soup s): ??????? case Box(Sandwich s): ??????? case Box(var s): ??????? case Bag(Soup s): ??????? case Bag(Sandwich s): ??????? case Bag(var s): ??? } So I think its similar at every level.? And you can say `var` or `Lunch` or `Object`; they're all total in this context. >> - use "var" so the total case is explicit,? "case var x", "case >> Foo(var x)", etc > Sure - this is a good solution. One minor annoying thing is that we > started from a position where we said "var is just for inference" and > now we're back in a place where, while? var is still, conceptually at > least, about inference, developers will also be using it for totality > (as inference guarantees that you are always 100% covered). No, you can also say `case Object x` or `case Foo(Object x)` and be semantically equivalent.? Var is just type inference here; Remi is saying "if you don't want to think about what types are total, say var, and you'll be right."? But its just inference for "the minimal type that is total." -------------- next part -------------- An HTML attachment was scrubbed... URL: From maurizio.cimadamore at oracle.com Wed Apr 28 15:40:27 2021 From: maurizio.cimadamore at oracle.com (Maurizio Cimadamore) Date: Wed, 28 Apr 2021 16:40:27 +0100 Subject: Switch labels (null again), some tweaking In-Reply-To: <76bf2826-b987-b606-271e-ad04c30d6509@oracle.com> References: <9ca67b52-70a1-b0a3-7da0-da96fa5d80ab@oracle.com> <996683411.1734289.1619567042958.JavaMail.zimbra@u-pem.fr> <969484d1-ce1e-e896-1b9c-675ce79567e9@oracle.com> <76bf2826-b987-b606-271e-ad04c30d6509@oracle.com> Message-ID: <0906e7cf-11a3-bcff-e7cd-d922bde7e3e0@oracle.com> On 28/04/2021 15:31, Brian Goetz wrote: > It depends on how much sealing there is.? Suppose we have sealed types > Container = Box|Bag and Lunch=Soup|Sandwich.? If we are switching on a > Container: > > ??? switch (lunch) { > ??????? case Box(Soup s): > ??????? case Box(Sandwich s): > ??????? case Bag(Soup s): > ??????? case Bag(Sandwich s): > ??? } > > This switch is exhaustive because: > ?- { Soup, Sandwich } is total on on Lunch (with remainder null) > ?- therefore { Box(Soup), Box(Sandwich) } is total on Box (with > remainder null, Box(null) } > ?- similarly { Bag(Soup), Bag(Sandwich) } is total on Bag (with > remainder null, Bag(null) } > ?- { Box, Bag } is total on Container (with remainder null) > ?- therefore the cases in our switch are exhaustive on Container I guess the case I'm referring to is: switch (lunch) { ??????? case Box(Soup s): ??????? case Bag(Soup s): } Where Soup is the only know subtype of Lunch. This code is exhaustive, but is Sandwich is added, it is no longer so. That said, if an error is issued whenever a switch statement is not exhaustive, that would alleviate my concerns - I think the error would cover this case also. The think the I find mildly odd is that when Sandwich is added, it might not be enough to add another `case`. If there was null-related logic inside `case Box(Soup s):` that would have to be moved somewhere else. So while in most cases it will be a straightforward refactoring, I can see some puzzlers emerging here. Maurizio From brian.goetz at oracle.com Wed Apr 28 15:54:52 2021 From: brian.goetz at oracle.com (Brian Goetz) Date: Wed, 28 Apr 2021 11:54:52 -0400 Subject: Switch labels (null again), some tweaking In-Reply-To: <0906e7cf-11a3-bcff-e7cd-d922bde7e3e0@oracle.com> References: <9ca67b52-70a1-b0a3-7da0-da96fa5d80ab@oracle.com> <996683411.1734289.1619567042958.JavaMail.zimbra@u-pem.fr> <969484d1-ce1e-e896-1b9c-675ce79567e9@oracle.com> <76bf2826-b987-b606-271e-ad04c30d6509@oracle.com> <0906e7cf-11a3-bcff-e7cd-d922bde7e3e0@oracle.com> Message-ID: <0c27f925-9686-e817-e3cd-db3ff9d55fea@oracle.com> > I guess the case I'm referring to is: > > switch (lunch) { > ??????? case Box(Soup s): > ??????? case Bag(Soup s): > } > > Where Soup is the only know subtype of Lunch. This code is exhaustive, > but is Sandwich is added, it is no longer so. Given Lunch is sealed to only permit Soup, and Container = Box|Bag, then this switch is exhaustive on COntainer. Later, when Lunch is extended to permit Sandwich, this switch becomes not exhaustive on Container, and you get a compile error, as you would want.? Now you can totalize either by handling the sandwich-related cases, OR by adding Box(Lunch)/Box(var) cases, OR by adding Container(Soup) cases, OR by adding Container(Lunch) case, OR by adding a default/var case. > That said, if an error is issued whenever a switch statement is not > exhaustive, that would alleviate my concerns - I think the error would > cover this case also. Yes.? Totality is compositional.? We can treat `Box(Soup)` as Box(var alpha) & alpha matches Soup.? Then the switch is a big disjunction of conjunctions, and we can rearrange with De Morgan's laws and related boolean algebra tricks.? So ?? Box(Soup) OR Box(Sandwich) ?? == (Box(var alpha) AND alpha matches Soup) OR (Box(var alpha) AND alpha matches Sandwich) ?? == Box(var alpha) AND (alpha matches Soup OR alpha matches Sandwich) ?? == Box(var alpha) AND (alpha matches Lunch) ?? == Box(Lunch) > The think the I find mildly odd is that when Sandwich is added, it > might not be enough to add another `case`. If there was null-related > logic inside `case Box(Soup s):` that would have to be moved somewhere > else. So while in most cases it will be a straightforward refactoring, > I can see some puzzlers emerging here. Can you pull on this string a big? From maurizio.cimadamore at oracle.com Wed Apr 28 16:21:04 2021 From: maurizio.cimadamore at oracle.com (Maurizio Cimadamore) Date: Wed, 28 Apr 2021 17:21:04 +0100 Subject: Switch labels (null again), some tweaking In-Reply-To: <0c27f925-9686-e817-e3cd-db3ff9d55fea@oracle.com> References: <9ca67b52-70a1-b0a3-7da0-da96fa5d80ab@oracle.com> <996683411.1734289.1619567042958.JavaMail.zimbra@u-pem.fr> <969484d1-ce1e-e896-1b9c-675ce79567e9@oracle.com> <76bf2826-b987-b606-271e-ad04c30d6509@oracle.com> <0906e7cf-11a3-bcff-e7cd-d922bde7e3e0@oracle.com> <0c27f925-9686-e817-e3cd-db3ff9d55fea@oracle.com> Message-ID: <4382a5fd-7046-29e8-d3c0-8b38ad7ab303@oracle.com> On 28/04/2021 16:54, Brian Goetz wrote: >> The think the I find mildly odd is that when Sandwich is added, it >> might not be enough to add another `case`. If there was null-related >> logic inside `case Box(Soup s):` that would have to be moved >> somewhere else. So while in most cases it will be a straightforward >> refactoring, I can see some puzzlers emerging here. > > Can you pull on this string a big? Let's say I start with this: switch (lunch) { ??????? case Box(Soup s) { ???????????? if (s == null) { ????????????????? System.err.println("Box of null"); ???????????? } else { ????????????????? System.err.println("Box of soup"); ???????????? } ??????? } ??????? case Bag(Soup s): { ? ? ? ?? ??? if (s == null) { ????????????????? System.err.println("Bag of null"); ???????????? } else { ????????????????? System.err.println("Bag of soup"); ???????????? } ??????? } } Then Sandwich is added to the hierarchy. The switch no longer compiles, I have to make it total. The following, which is, I believe, the first thing that will come to mind: switch (lunch) { ??????? case Box(Soup s) {? /* same as before */ } ??????? case Box(Sandwich s) { ... } ??????? case Bag(Soup s): { /* same as before */ } ??????? case Bag(Sandwich s) { ... } } Now, question: is the above switch considered exhaustive? If not (because Box(null) is no longer matched), fine, the user will at least have some clue of what's happening. But if it is considered exhaustive, then this will compile, but the null handling logic will be in the wrong place, and will be essentially dead code (which the user might be unaware of). Assuming the above is not exhaustive, the user might then think to tweak to do this: switch (lunch) { ??????? case Box(Soup s) {? /* same as before */ } ??????? case Box(Sandwich s) { ... } ??????? case Bag(Soup s): { /* same as before */ } ??????? case Bag(Sandwich s) { ... } ??????? default: ... } But that's again incorrect. We still have to move the null handling logic from `case Box(Soup)` and `case Box(Bar)` - so, the above code doesn't really cut it, since the `null` is nested, and not at the toplevel. So the better form would be this: switch (lunch) { ??????? case Box(Soup s) { System.err.println("Box of soup"); } ??????? case Box(Sandwich s) { System.err.println("Box of sandwich"); } ??????? case Box(var x) { System.err.println("Box of null"); } ??????? case Bag(Soup s): { System.err.println("Bag of soup"); } ??????? case Bag(Sandwich s) { System.err.println("Bag of sandwich"); } ??????? case Bag(var x) { System.err.println("Bag of null"); } } I believe this is finally (1) total and (2) preserve the semantics of the original code. But it took quite a while to get here. And, in this new code, it seems like we're essentially using `case Box(var x)` (or `case Box(Object o)` it's the same) to really mean `case Box(null)`. Which is brittle - because, with this new code, if a new subtype is added (Pizza), the switch will remain total, no error would occur, but the could would behave incorrectly because what used to be a "catch-null" case has now turned into more than that. Maurizio -------------- next part -------------- An HTML attachment was scrubbed... URL: From brian.goetz at oracle.com Wed Apr 28 16:29:31 2021 From: brian.goetz at oracle.com (Brian Goetz) Date: Wed, 28 Apr 2021 12:29:31 -0400 Subject: Switch labels (null again), some tweaking In-Reply-To: <4382a5fd-7046-29e8-d3c0-8b38ad7ab303@oracle.com> References: <9ca67b52-70a1-b0a3-7da0-da96fa5d80ab@oracle.com> <996683411.1734289.1619567042958.JavaMail.zimbra@u-pem.fr> <969484d1-ce1e-e896-1b9c-675ce79567e9@oracle.com> <76bf2826-b987-b606-271e-ad04c30d6509@oracle.com> <0906e7cf-11a3-bcff-e7cd-d922bde7e3e0@oracle.com> <0c27f925-9686-e817-e3cd-db3ff9d55fea@oracle.com> <4382a5fd-7046-29e8-d3c0-8b38ad7ab303@oracle.com> Message-ID: <36a7b436-251c-805c-59b3-4d153324c5af@oracle.com> > Let's say I start with this: > > > switch (lunch) { > ??????? case Box(Soup s) { > ???????????? if (s == null) { > ????????????????? System.err.println("Box of null"); > ???????????? } else { > ????????????????? System.err.println("Box of soup"); > ???????????? } > ??????? } > > ??????? case Bag(Soup s): { > ? ? ? ?? ??? if (s == null) { > ????????????????? System.err.println("Bag of null"); > ???????????? } else { > ????????????????? System.err.println("Bag of soup"); > ???????????? } > > ??????? } > > } > I assume that you are saying Box permits Soup only.? But your assumptions about "where do the nulls go" here are not right. Box(Soup) does not match Box(null); the set of patterns { Box(Soup) } is total on Box(Lunch) _with remainder Box(null)_.? So the null paths in this example are dead.? (Also missing break statements.)? So rewriting, this switch is really equivalent to: ??? switch (lunch) { ??????? case Box(Soup s): ????????????? System.err.println("Box of soup"); ????????????? break; ??????? case Bag(Soup s): ???????????? System.err.println("Bag of soup"); ???????????? break; ??????? /* implicit */ ??????? case Box(null), Bag(null): throw new NPE(); ??? } and the switch is total on Container(Lunch) under the Lunch=Soup, Container=Box|Bag assumptions. > Then Sandwich is added to the hierarchy. The switch no longer > compiles, I have to make it total. The following, which is, I believe, > the first thing that will come to mind: > > switch (lunch) { > ??????? case Box(Soup s) {? /* same as before */ } > ??????? case Box(Sandwich s) { ... } > ??????? case Bag(Soup s): { /* same as before */ } > ??????? case Bag(Sandwich s) { ... } > } > With the correct version of the first switch, your first idea for fixing it to accomodate sandwiches is correct!? The switch is now total on Container, the old cases handle the same thing they did, the new cases handle the new cases, and the implicit cases (Box(null) etc) handle the same thing they did. > But if it is considered exhaustive, then this will compile, but the > null handling logic will be in the wrong place, and will be > essentially dead code (which the user might be unaware of). No, because it was dead in the first place too.? Box(null) and Bag(null) were always treated as remainder. From maurizio.cimadamore at oracle.com Wed Apr 28 16:33:53 2021 From: maurizio.cimadamore at oracle.com (Maurizio Cimadamore) Date: Wed, 28 Apr 2021 17:33:53 +0100 Subject: Switch labels (null again), some tweaking In-Reply-To: <36a7b436-251c-805c-59b3-4d153324c5af@oracle.com> References: <9ca67b52-70a1-b0a3-7da0-da96fa5d80ab@oracle.com> <996683411.1734289.1619567042958.JavaMail.zimbra@u-pem.fr> <969484d1-ce1e-e896-1b9c-675ce79567e9@oracle.com> <76bf2826-b987-b606-271e-ad04c30d6509@oracle.com> <0906e7cf-11a3-bcff-e7cd-d922bde7e3e0@oracle.com> <0c27f925-9686-e817-e3cd-db3ff9d55fea@oracle.com> <4382a5fd-7046-29e8-d3c0-8b38ad7ab303@oracle.com> <36a7b436-251c-805c-59b3-4d153324c5af@oracle.com> Message-ID: <1fd988d4-9749-20fd-e281-06e0a8b3cfe3@oracle.com> On 28/04/2021 17:29, Brian Goetz wrote: > I assume that you are saying Box permits Soup only.? But your > assumptions about "where do the nulls go" here are not right. > Box(Soup) does not match Box(null); the set of patterns { Box(Soup) } > is total on Box(Lunch) _with remainder Box(null)_.? So the null paths > in this example are dead.? (Also missing break statements.)? So > rewriting, this switch is really equivalent to: > > ??? switch (lunch) { > ??????? case Box(Soup s): > ????????????? System.err.println("Box of soup"); > ????????????? break; > > ??????? case Bag(Soup s): > ???????????? System.err.println("Bag of soup"); > ???????????? break; > > ??????? /* implicit */ > ??????? case Box(null), Bag(null): throw new NPE(); > ??? } > > and the switch is total on Container(Lunch) under the Lunch=Soup, > Container=Box|Bag assumptions. I have to admit that this is surprising. So, if I have a sealed hierarchy that only permits one concrete type: interface Foo permits Bar doing: Foo x = ... if (x instanceof Bar) is not considered a total instanceof? Maurizio From brian.goetz at oracle.com Wed Apr 28 16:48:52 2021 From: brian.goetz at oracle.com (Brian Goetz) Date: Wed, 28 Apr 2021 12:48:52 -0400 Subject: Switch labels (null again), some tweaking In-Reply-To: <1fd988d4-9749-20fd-e281-06e0a8b3cfe3@oracle.com> References: <9ca67b52-70a1-b0a3-7da0-da96fa5d80ab@oracle.com> <996683411.1734289.1619567042958.JavaMail.zimbra@u-pem.fr> <969484d1-ce1e-e896-1b9c-675ce79567e9@oracle.com> <76bf2826-b987-b606-271e-ad04c30d6509@oracle.com> <0906e7cf-11a3-bcff-e7cd-d922bde7e3e0@oracle.com> <0c27f925-9686-e817-e3cd-db3ff9d55fea@oracle.com> <4382a5fd-7046-29e8-d3c0-8b38ad7ab303@oracle.com> <36a7b436-251c-805c-59b3-4d153324c5af@oracle.com> <1fd988d4-9749-20fd-e281-06e0a8b3cfe3@oracle.com> Message-ID: The set of patterns { Box(Soup) } is considered total on Box _with remainder { null, Box(null), Box(novel) } _. The pattern Box(Soup) on its own is total on Box (as opposed to Box), with remainder { null }; we'll still NPE if the Box itself is null. The intuition here is that Lunch is still a more abstract type than Soup, even if the implementation says "only soup here".? We know that this assumption could be violated before we get to runtime. A normalizing force here is we want the same thing at top level and nested level.? If i have: ??? Lunch lunch = ... ??? switch(lunch) { ??????? case Soup s: ??? } I should expect the same null treatment as I do in the lifted case: ??? Box box = ... ??? switch (box) { ??????? case Box(Soup s): ... ??? } Which we get!? In the first case, the _single pattern_ Soup is not individually total on Lunch (Lunch is more abstract), but the _set of patterns_ { Soup } is total on Lunch with remainder { null, novel subtype of Lunch }, due to sealing, so we are able to conclude the switch itself is exhaustive (with some remainder rejected.)?? When we lift, we get totality with remainder of { Box(null), Box(novel) }, plus also { null } because of the lifting. On 4/28/2021 12:33 PM, Maurizio Cimadamore wrote: > > On 28/04/2021 17:29, Brian Goetz wrote: >> I assume that you are saying Box permits Soup only.? But your >> assumptions about "where do the nulls go" here are not right. >> Box(Soup) does not match Box(null); the set of patterns { Box(Soup) } >> is total on Box(Lunch) _with remainder Box(null)_.? So the null paths >> in this example are dead.? (Also missing break statements.)? So >> rewriting, this switch is really equivalent to: >> >> ??? switch (lunch) { >> ??????? case Box(Soup s): >> ????????????? System.err.println("Box of soup"); >> ????????????? break; >> >> ??????? case Bag(Soup s): >> ???????????? System.err.println("Bag of soup"); >> ???????????? break; >> >> ??????? /* implicit */ >> ??????? case Box(null), Bag(null): throw new NPE(); >> ??? } >> >> and the switch is total on Container(Lunch) under the Lunch=Soup, >> Container=Box|Bag assumptions. > > I have to admit that this is surprising. > > So, if I have a sealed hierarchy that only permits one concrete type: > > interface Foo permits Bar > > doing: > > Foo x = ... > if (x instanceof Bar) > > is not considered a total instanceof? > > Maurizio > > > -------------- next part -------------- An HTML attachment was scrubbed... URL: From forax at univ-mlv.fr Wed Apr 28 17:03:18 2021 From: forax at univ-mlv.fr (forax at univ-mlv.fr) Date: Wed, 28 Apr 2021 19:03:18 +0200 (CEST) Subject: Switch labels (null again), some tweaking In-Reply-To: <1fd988d4-9749-20fd-e281-06e0a8b3cfe3@oracle.com> References: <969484d1-ce1e-e896-1b9c-675ce79567e9@oracle.com> <76bf2826-b987-b606-271e-ad04c30d6509@oracle.com> <0906e7cf-11a3-bcff-e7cd-d922bde7e3e0@oracle.com> <0c27f925-9686-e817-e3cd-db3ff9d55fea@oracle.com> <4382a5fd-7046-29e8-d3c0-8b38ad7ab303@oracle.com> <36a7b436-251c-805c-59b3-4d153324c5af@oracle.com> <1fd988d4-9749-20fd-e281-06e0a8b3cfe3@oracle.com> Message-ID: <649864065.2237766.1619629398538.JavaMail.zimbra@u-pem.fr> ----- Mail original ----- > De: "Maurizio Cimadamore" > ?: "Brian Goetz" , "Remi Forax" > Cc: "amber-spec-experts" > Envoy?: Mercredi 28 Avril 2021 18:33:53 > Objet: Re: Switch labels (null again), some tweaking > On 28/04/2021 17:29, Brian Goetz wrote: >> I assume that you are saying Box permits Soup only.? But your >> assumptions about "where do the nulls go" here are not right. >> Box(Soup) does not match Box(null); the set of patterns { Box(Soup) } >> is total on Box(Lunch) _with remainder Box(null)_.? So the null paths >> in this example are dead.? (Also missing break statements.)? So >> rewriting, this switch is really equivalent to: >> >> ??? switch (lunch) { >> ??????? case Box(Soup s): >> ????????????? System.err.println("Box of soup"); >> ????????????? break; >> >> ??????? case Bag(Soup s): >> ???????????? System.err.println("Bag of soup"); >> ???????????? break; >> >> ??????? /* implicit */ >> ??????? case Box(null), Bag(null): throw new NPE(); >> ??? } >> >> and the switch is total on Container(Lunch) under the Lunch=Soup, >> Container=Box|Bag assumptions. > > I have to admit that this is surprising. > > So, if I have a sealed hierarchy that only permits one concrete type: > > interface Foo permits Bar > > doing: > > Foo x = ... > if (x instanceof Bar) > > is not considered a total instanceof? There is no notion of totality for instanceof. For a switch, the concept of totality exist because when you have a switch you have two ways of seeing a switch as a cascade of "if ... instanceof". The question is should the last case be the equivament of "else", or not ? By example, switch(x) { case Foo foo: case Bar bar: } can be seen either as if (x instanceof Foo foo) { ... } if (x instanceof Bar bar) { ... } or if (x instanceof Foo foo) { ... } else { ... } If x is declared as a Bar, then "case Bar bar" is total, so the later translation/semantics is used, if Foo and Bar are subtypes of a sealed type, "case Bar bar" is not total, the former semantics is used. And as Brian said, it also works the same way with sub-patterns i.e. it composes well. > > Maurizio R?mi From maurizio.cimadamore at oracle.com Wed Apr 28 17:05:47 2021 From: maurizio.cimadamore at oracle.com (Maurizio Cimadamore) Date: Wed, 28 Apr 2021 18:05:47 +0100 Subject: Switch labels (null again), some tweaking In-Reply-To: References: <9ca67b52-70a1-b0a3-7da0-da96fa5d80ab@oracle.com> <996683411.1734289.1619567042958.JavaMail.zimbra@u-pem.fr> <969484d1-ce1e-e896-1b9c-675ce79567e9@oracle.com> <76bf2826-b987-b606-271e-ad04c30d6509@oracle.com> <0906e7cf-11a3-bcff-e7cd-d922bde7e3e0@oracle.com> <0c27f925-9686-e817-e3cd-db3ff9d55fea@oracle.com> <4382a5fd-7046-29e8-d3c0-8b38ad7ab303@oracle.com> <36a7b436-251c-805c-59b3-4d153324c5af@oracle.com> <1fd988d4-9749-20fd-e281-06e0a8b3cfe3@oracle.com> Message-ID: <3e1df19a-1295-d69d-3ab3-bcc741623f01@oracle.com> On 28/04/2021 17:48, Brian Goetz wrote: > The set of patterns { Box(Soup) } is considered total on Box > _with remainder { null, Box(null), Box(novel) } _. > > The pattern Box(Soup) on its own is total on Box (as opposed to > Box), with remainder { null }; we'll still NPE if the Box > itself is null. > > The intuition here is that Lunch is still a more abstract type than > Soup, even if the implementation says "only soup here".? We know that > this assumption could be violated before we get to runtime. I think I get this - but I guess this implies that, in my previous example: Foo x = ... if (x instanceof Bar) The instanceof will not be considered total, and therefore be accepted by the compiler (sorry to repeat the same question - I want to make sure I understand how totality works with sealed hierarchies). If that's the case, I find that a bit odd - because enums kind of have the same issue (but we have opted to trust that a switch on an enum is total if all the constants known at compile time are covered) - and, to my eyes, if you squint, a sealed hierarchy is like an enum for types (e.g. sum type). That said, I think this discussion kind of demonstrates that reasoning about totality is not that straightforward; I believe you can see why I jumped to the conclusion that Box must have been total on Box (if Soup was the only permitted concrete subtype of Lunch) - but it turned out I was barking up the wrong tree. This is surely caused, in part, by the fact that I'm less immersed in pattern-land (e.g. more an interested follower at a distance); but I think it also reflects some genuine complexity when it comes to reasoning about totality. Anyway, backing up - this below: ``` switch (lunch) { ??????? case Box(Soup s): ????????????? System.err.println("Box of soup"); ????????????? break; ??????? case Bag(Soup s): ???????????? System.err.println("Bag of soup"); ???????????? break; ??????? /* implicit */ ??????? case Box(null), Bag(null): throw new NPE(); ??? } ``` is good code, which says what it means. I think the challenge will be to present error messages (e.g. if the user forgot to add case Box(null)) in a way that makes it clear to the user as to what's missing; and maybe that will be enough. Maurizio > > A normalizing force here is we want the same thing at top level and > nested level.? If i have: > > ??? Lunch lunch = ... > ??? switch(lunch) { > ??????? case Soup s: > ??? } > > I should expect the same null treatment as I do in the lifted case: > > ??? Box box = ... > ??? switch (box) { > ??????? case Box(Soup s): ... > ??? } > > Which we get!? In the first case, the _single pattern_ Soup is not > individually total on Lunch (Lunch is more abstract), but the _set of > patterns_ { Soup } is total on Lunch with remainder { null, novel > subtype of Lunch }, due to sealing, so we are able to conclude the > switch itself is exhaustive (with some remainder rejected.)?? When we > lift, we get totality with remainder of { Box(null), Box(novel) }, > plus also { null } because of the lifting. > > > > > > On 4/28/2021 12:33 PM, Maurizio Cimadamore wrote: >> >> On 28/04/2021 17:29, Brian Goetz wrote: >>> I assume that you are saying Box permits Soup only.? But your >>> assumptions about "where do the nulls go" here are not right. >>> Box(Soup) does not match Box(null); the set of patterns { Box(Soup) >>> } is total on Box(Lunch) _with remainder Box(null)_.? So the null >>> paths in this example are dead.? (Also missing break statements.)? >>> So rewriting, this switch is really equivalent to: >>> >>> ??? switch (lunch) { >>> ??????? case Box(Soup s): >>> ????????????? System.err.println("Box of soup"); >>> ????????????? break; >>> >>> ??????? case Bag(Soup s): >>> ???????????? System.err.println("Bag of soup"); >>> ???????????? break; >>> >>> ??????? /* implicit */ >>> ??????? case Box(null), Bag(null): throw new NPE(); >>> ??? } >>> >>> and the switch is total on Container(Lunch) under the Lunch=Soup, >>> Container=Box|Bag assumptions. >> >> I have to admit that this is surprising. >> >> So, if I have a sealed hierarchy that only permits one concrete type: >> >> interface Foo permits Bar >> >> doing: >> >> Foo x = ... >> if (x instanceof Bar) >> >> is not considered a total instanceof? >> >> Maurizio >> >> >> > -------------- next part -------------- An HTML attachment was scrubbed... URL: From maurizio.cimadamore at oracle.com Wed Apr 28 17:09:07 2021 From: maurizio.cimadamore at oracle.com (Maurizio Cimadamore) Date: Wed, 28 Apr 2021 18:09:07 +0100 Subject: Switch labels (null again), some tweaking In-Reply-To: <649864065.2237766.1619629398538.JavaMail.zimbra@u-pem.fr> References: <969484d1-ce1e-e896-1b9c-675ce79567e9@oracle.com> <76bf2826-b987-b606-271e-ad04c30d6509@oracle.com> <0906e7cf-11a3-bcff-e7cd-d922bde7e3e0@oracle.com> <0c27f925-9686-e817-e3cd-db3ff9d55fea@oracle.com> <4382a5fd-7046-29e8-d3c0-8b38ad7ab303@oracle.com> <36a7b436-251c-805c-59b3-4d153324c5af@oracle.com> <1fd988d4-9749-20fd-e281-06e0a8b3cfe3@oracle.com> <649864065.2237766.1619629398538.JavaMail.zimbra@u-pem.fr> Message-ID: <95a78cbf-39c9-4fcd-760a-b364e125d9c0@oracle.com> On 28/04/2021 18:03, forax at univ-mlv.fr wrote: > There is no notion of totality for instanceof. This is not what I understood when reading Brian message: > We made a decision to lump pattern matching in with `instanceof` > because it seemed silly to have two almost identical but subtly > different constructs for "dynamic type test" and "pattern match" > (given that you can do dynamic type tests with patterns.)? We knew > that this would have some uncomfortable consequences, and what we have > tentatively decided to do is outlaw total patterns in instanceof, so > that users are not confronted with the subtle difference between `x > instanceof Object` and `x instanceof Object o`.? This may not be a > totally satisfying answer, and we left some room to adjust this, but > its where we are. instanceof featuring a pattern that is total w.r.t. the type of the instanceof expression is outlawed. Which is why I was bringing that up. Maurizio -------------- next part -------------- An HTML attachment was scrubbed... URL: From forax at univ-mlv.fr Wed Apr 28 17:12:37 2021 From: forax at univ-mlv.fr (forax at univ-mlv.fr) Date: Wed, 28 Apr 2021 19:12:37 +0200 (CEST) Subject: Switch labels (null again), some tweaking In-Reply-To: <95a78cbf-39c9-4fcd-760a-b364e125d9c0@oracle.com> References: <0906e7cf-11a3-bcff-e7cd-d922bde7e3e0@oracle.com> <0c27f925-9686-e817-e3cd-db3ff9d55fea@oracle.com> <4382a5fd-7046-29e8-d3c0-8b38ad7ab303@oracle.com> <36a7b436-251c-805c-59b3-4d153324c5af@oracle.com> <1fd988d4-9749-20fd-e281-06e0a8b3cfe3@oracle.com> <649864065.2237766.1619629398538.JavaMail.zimbra@u-pem.fr> <95a78cbf-39c9-4fcd-760a-b364e125d9c0@oracle.com> Message-ID: <178945583.2240887.1619629957607.JavaMail.zimbra@u-pem.fr> > De: "Maurizio Cimadamore" > ?: "Remi Forax" > Cc: "Brian Goetz" , "amber-spec-experts" > > Envoy?: Mercredi 28 Avril 2021 19:09:07 > Objet: Re: Switch labels (null again), some tweaking > On 28/04/2021 18:03, [ mailto:forax at univ-mlv.fr | forax at univ-mlv.fr ] wrote: >> There is no notion of totality for instanceof. > This is not what I understood when reading Brian message: >> We made a decision to lump pattern matching in with `instanceof` because it >> seemed silly to have two almost identical but subtly different constructs for >> "dynamic type test" and "pattern match" (given that you can do dynamic type >> tests with patterns.) We knew that this would have some uncomfortable >> consequences, and what we have tentatively decided to do is outlaw total >> patterns in instanceof, so that users are not confronted with the subtle >> difference between `x instanceof Object` and `x instanceof Object o`. This may >> not be a totally satisfying answer, and we left some room to adjust this, but >> its where we are. > instanceof featuring a pattern that is total w.r.t. the type of the instanceof > expression is outlawed. Which is why I was bringing that up. Let me re-phrase it, there is no need for a notion of totality on instanceof. (at least at the first level, we need it for sub-patterns) > Maurizio R?mi -------------- next part -------------- An HTML attachment was scrubbed... URL: From brian.goetz at oracle.com Wed Apr 28 17:27:16 2021 From: brian.goetz at oracle.com (Brian Goetz) Date: Wed, 28 Apr 2021 13:27:16 -0400 Subject: Switch labels (null again), some tweaking In-Reply-To: <3e1df19a-1295-d69d-3ab3-bcc741623f01@oracle.com> References: <9ca67b52-70a1-b0a3-7da0-da96fa5d80ab@oracle.com> <996683411.1734289.1619567042958.JavaMail.zimbra@u-pem.fr> <969484d1-ce1e-e896-1b9c-675ce79567e9@oracle.com> <76bf2826-b987-b606-271e-ad04c30d6509@oracle.com> <0906e7cf-11a3-bcff-e7cd-d922bde7e3e0@oracle.com> <0c27f925-9686-e817-e3cd-db3ff9d55fea@oracle.com> <4382a5fd-7046-29e8-d3c0-8b38ad7ab303@oracle.com> <36a7b436-251c-805c-59b3-4d153324c5af@oracle.com> <1fd988d4-9749-20fd-e281-06e0a8b3cfe3@oracle.com> <3e1df19a-1295-d69d-3ab3-bcc741623f01@oracle.com> Message-ID: <36846119-637e-ab86-21c3-0ec68f333111@oracle.com> I think part of the problem is that we're using the word "total" in different ways. A pattern P may be total on type T with remainder R.? For example, the pattern `Soup s` is total on `Soup` with no remainder. A _set_ of patterns may be total on T with remainder R as well. (The only way a set of patterns is total is either (a) one of the patterns in the set is already total on T, OR (b) sealing comes into play.)? Maybe this should be called "exhaustive" to separate from pattern totality. Switch exhaustiveness derives from set-totality. Instanceof prohibits patterns that are total without remainder, for two reasons: (1) its silly to ask a question which constant-folds to `true`, and (b) the disagreement between traditional `instanceof Object` and `instanceof ` at null would likely be a source of bugs.? (This was the cost of reusing instanceof rather than creating a new "matches" operator.) > Foo x = ... > if (x instanceof Bar) > > The instanceof will not be considered total, and therefore be accepted > by the compiler (sorry to repeat the same question - I want to make > sure I understand how totality works with sealed hierarchies). > If the RHS of an `instanceof` is a type (not a type pattern), then this has traditional `instanceof` behavior.? If Bar <: Foo, then this is in effect a null check. If the RHS is a _pattern_, then the pattern must not be total without remainder.? If Bar <: Foo, `Bar b` is total on Foo, so the compiler says "dumb question, ask a different one." If the RHS is a non-total pattern, or a total pattern with remainder, then there's a real question being asked.? So in your Lunch-permits-Soup example, you could say ??? if (lunch instanceof Soup s) and this matches _on non-null lunch_.? Just like the switch. The only difference is switch will throw on unmatched nulls, whereas instanceof says "no, not an instance", but that's got nothing to do with patterns, it's about conditional constructs. > If that's the case, I find that a bit odd - because enums kind of have > the same issue (but we have opted to trust that a switch on an enum is > total if all the constants known at compile time are covered) - and, > to my eyes, if you squint, a sealed hierarchy is like an enum for > types (e.g. sum type). > OK, let's talk about enums. Suppose I have: ??? enum Lunch { Soup } and I do ??? switch (lunch) { ??????? case Soup -> ... ??? } What happens on null?? It throws, and it always has.? The behavior for the sealed analogue is the same; the `Soup s` pattern matches the non-null lunches, and if null is left unhandled elsewhere in the switch, the switch throws.? No asymmetry. > Anyway, backing up - this below: > > ``` > > switch (lunch) { > ??????? case Box(Soup s): > ????????????? System.err.println("Box of soup"); > ????????????? break; > > ??????? case Bag(Soup s): > ???????????? System.err.println("Bag of soup"); > ???????????? break; > > ??????? /* implicit */ > ??????? case Box(null), Bag(null): throw new NPE(); > ??? } > ``` > > is good code, which says what it means. I think the challenge will be > to present error messages (e.g. if the user forgot to add case > Box(null)) in a way that makes it clear to the user as to what's > missing; and maybe that will be enough. > The challenge here is that we don't want to force the user to handle the "silly" cases, such as: ??? Boolean bool = ... ??? switch (bool) { ??????? case true -> ... ??????? case false -> ... ??????? case null -> ...? // who would be happy about having to write this case? ??? } and the similarly lifted: ??? Box box = ... ??? switch (box) { ??????? case Box(true) -> ... ??????? case Box(false) -> ... ??????? case Box(null) -> ...? // who would be happy about having to write this case? ??????? case Box(novel) -> ... // or this case? ??????? case null ->?????????? // or this case? ??? } So we define the "remainder" as the values that "fall into the cracks between the patterns."? Users can write patterns for these, and they'll match, but if not, the compiler inserts code to catch these and throw something. The benefit is twofold: not only does the user not have to write the stupid cases (imagine if Box had ten slots, would we want to write the 2^10 partial null cases?), but because we throw on the remainder, DA can treat the switch as covering all boxes, and be assured there are no leaks. -------------- next part -------------- An HTML attachment was scrubbed... URL: From brian.goetz at oracle.com Wed Apr 28 17:34:43 2021 From: brian.goetz at oracle.com (Brian Goetz) Date: Wed, 28 Apr 2021 13:34:43 -0400 Subject: Switch labels (null again), some tweaking In-Reply-To: <36846119-637e-ab86-21c3-0ec68f333111@oracle.com> References: <9ca67b52-70a1-b0a3-7da0-da96fa5d80ab@oracle.com> <996683411.1734289.1619567042958.JavaMail.zimbra@u-pem.fr> <969484d1-ce1e-e896-1b9c-675ce79567e9@oracle.com> <76bf2826-b987-b606-271e-ad04c30d6509@oracle.com> <0906e7cf-11a3-bcff-e7cd-d922bde7e3e0@oracle.com> <0c27f925-9686-e817-e3cd-db3ff9d55fea@oracle.com> <4382a5fd-7046-29e8-d3c0-8b38ad7ab303@oracle.com> <36a7b436-251c-805c-59b3-4d153324c5af@oracle.com> <1fd988d4-9749-20fd-e281-06e0a8b3cfe3@oracle.com> <3e1df19a-1295-d69d-3ab3-bcc741623f01@oracle.com> <36846119-637e-ab86-21c3-0ec68f333111@oracle.com> Message-ID: > The benefit is twofold: not only does the user not have to write the > stupid cases (imagine if Box had ten slots, would we want to write the > 2^10 partial null cases?), but because we throw on the remainder, DA > can treat the switch as covering all boxes, and be assured there are > no leaks. More on this last point.? We don't yet have pattern assignment, but totality and remainder are key to it.? Suppose we have ??? Box bs = ... and we want to destructure: ??? Box(String s) = bs; ??? // s had better be DA here The pattern on the LHS must be total, but can have remainder; the statement is allowed to throw on the remainder, because the remainder is, by definition, the "silly" matches.? Here, the only remainder is null; you can't destructure a null, so if you asked to destructure a Box, you probably were assuming that it was a real box.? So this assignment NPEs on null (not unlike the corresponding switch). -------------- next part -------------- An HTML attachment was scrubbed... URL: From maurizio.cimadamore at oracle.com Wed Apr 28 18:09:46 2021 From: maurizio.cimadamore at oracle.com (Maurizio Cimadamore) Date: Wed, 28 Apr 2021 19:09:46 +0100 Subject: Switch labels (null again), some tweaking In-Reply-To: <36846119-637e-ab86-21c3-0ec68f333111@oracle.com> References: <9ca67b52-70a1-b0a3-7da0-da96fa5d80ab@oracle.com> <996683411.1734289.1619567042958.JavaMail.zimbra@u-pem.fr> <969484d1-ce1e-e896-1b9c-675ce79567e9@oracle.com> <76bf2826-b987-b606-271e-ad04c30d6509@oracle.com> <0906e7cf-11a3-bcff-e7cd-d922bde7e3e0@oracle.com> <0c27f925-9686-e817-e3cd-db3ff9d55fea@oracle.com> <4382a5fd-7046-29e8-d3c0-8b38ad7ab303@oracle.com> <36a7b436-251c-805c-59b3-4d153324c5af@oracle.com> <1fd988d4-9749-20fd-e281-06e0a8b3cfe3@oracle.com> <3e1df19a-1295-d69d-3ab3-bcc741623f01@oracle.com> <36846119-637e-ab86-21c3-0ec68f333111@oracle.com> Message-ID: <769f1882-37dc-b3fe-540f-288147a6a67b@oracle.com> I think I got the two main fallacies which led me down the wrong path: 1. there is a distinction between patterns that are total on T, and patterns that are total on T, but with a "remainder" 2. there is a distinction between a pattern being total on T, and a set of patterns being total (or exhaustive) on T This sent me completely haywire, because I was trying to reason in terms of what a plain pattern instanceof would consider "total", and then translate the results to nested patterns in switch - and that didn't work, because two different sets of rules apply there. Maurizio On 28/04/2021 18:27, Brian Goetz wrote: > I think part of the problem is that we're using the word "total" in > different ways. > > A pattern P may be total on type T with remainder R.? For example, the > pattern `Soup s` is total on `Soup` with no remainder. > > A _set_ of patterns may be total on T with remainder R as well.? (The > only way a set of patterns is total is either (a) one of the patterns > in the set is already total on T, OR (b) sealing comes into play.)? > Maybe this should be called "exhaustive" to separate from pattern > totality. > > Switch exhaustiveness derives from set-totality. > > Instanceof prohibits patterns that are total without remainder, for > two reasons: (1) its silly to ask a question which constant-folds to > `true`, and (b) the disagreement between traditional `instanceof > Object` and `instanceof ` at null would likely be a > source of bugs.? (This was the cost of reusing instanceof rather than > creating a new "matches" operator.) > >> Foo x = ... >> if (x instanceof Bar) >> >> The instanceof will not be considered total, and therefore be >> accepted by the compiler (sorry to repeat the same question - I want >> to make sure I understand how totality works with sealed hierarchies). >> > > If the RHS of an `instanceof` is a type (not a type pattern), then > this has traditional `instanceof` behavior.? If Bar <: Foo, then this > is in effect a null check. > > If the RHS is a _pattern_, then the pattern must not be total without > remainder.? If Bar <: Foo, `Bar b` is total on Foo, so the compiler > says "dumb question, ask a different one." > > If the RHS is a non-total pattern, or a total pattern with remainder, > then there's a real question being asked.? So in your > Lunch-permits-Soup example, you could say > > ??? if (lunch instanceof Soup s) > > and this matches _on non-null lunch_.? Just like the switch. The only > difference is switch will throw on unmatched nulls, whereas instanceof > says "no, not an instance", but that's got nothing to do with > patterns, it's about conditional constructs. > >> If that's the case, I find that a bit odd - because enums kind of >> have the same issue (but we have opted to trust that a switch on an >> enum is total if all the constants known at compile time are covered) >> - and, to my eyes, if you squint, a sealed hierarchy is like an enum >> for types (e.g. sum type). >> > > OK, let's talk about enums.? Suppose I have: > > ??? enum Lunch { Soup } > > and I do > > ??? switch (lunch) { > ??????? case Soup -> ... > ??? } > > What happens on null?? It throws, and it always has.? The behavior for > the sealed analogue is the same; the `Soup s` pattern matches the > non-null lunches, and if null is left unhandled elsewhere in the > switch, the switch throws.? No asymmetry. > >> Anyway, backing up - this below: >> >> ``` >> >> switch (lunch) { >> ??????? case Box(Soup s): >> ????????????? System.err.println("Box of soup"); >> ????????????? break; >> >> ??????? case Bag(Soup s): >> ???????????? System.err.println("Bag of soup"); >> ???????????? break; >> >> ??????? /* implicit */ >> ??????? case Box(null), Bag(null): throw new NPE(); >> ??? } >> ``` >> >> is good code, which says what it means. I think the challenge will be >> to present error messages (e.g. if the user forgot to add case >> Box(null)) in a way that makes it clear to the user as to what's >> missing; and maybe that will be enough. >> > > The challenge here is that we don't want to force the user to handle > the "silly" cases, such as: > > ??? Boolean bool = ... > ??? switch (bool) { > ??????? case true -> ... > ??????? case false -> ... > ??????? case null -> ...? // who would be happy about having to write > this case? > ??? } > > and the similarly lifted: > > ??? Box box = ... > ??? switch (box) { > ??????? case Box(true) -> ... > ??????? case Box(false) -> ... > ??????? case Box(null) -> ...? // who would be happy about having to > write this case? > ??????? case Box(novel) -> ... // or this case? > ??????? case null ->?????????? // or this case? > ??? } > > So we define the "remainder" as the values that "fall into the cracks > between the patterns."? Users can write patterns for these, and > they'll match, but if not, the compiler inserts code to catch these > and throw something. > > The benefit is twofold: not only does the user not have to write the > stupid cases (imagine if Box had ten slots, would we want to write the > 2^10 partial null cases?), but because we throw on the remainder, DA > can treat the switch as covering all boxes, and be assured there are > no leaks. > -------------- next part -------------- An HTML attachment was scrubbed... URL: From brian.goetz at oracle.com Wed Apr 28 18:13:49 2021 From: brian.goetz at oracle.com (Brian Goetz) Date: Wed, 28 Apr 2021 14:13:49 -0400 Subject: Switch labels (null again), some tweaking In-Reply-To: <769f1882-37dc-b3fe-540f-288147a6a67b@oracle.com> References: <9ca67b52-70a1-b0a3-7da0-da96fa5d80ab@oracle.com> <996683411.1734289.1619567042958.JavaMail.zimbra@u-pem.fr> <969484d1-ce1e-e896-1b9c-675ce79567e9@oracle.com> <76bf2826-b987-b606-271e-ad04c30d6509@oracle.com> <0906e7cf-11a3-bcff-e7cd-d922bde7e3e0@oracle.com> <0c27f925-9686-e817-e3cd-db3ff9d55fea@oracle.com> <4382a5fd-7046-29e8-d3c0-8b38ad7ab303@oracle.com> <36a7b436-251c-805c-59b3-4d153324c5af@oracle.com> <1fd988d4-9749-20fd-e281-06e0a8b3cfe3@oracle.com> <3e1df19a-1295-d69d-3ab3-bcc741623f01@oracle.com> <36846119-637e-ab86-21c3-0ec68f333111@oracle.com> <769f1882-37dc-b3fe-540f-288147a6a67b@oracle.com> Message-ID: I think you're right that the terminology is currently biased for mathematicians, which is a necessary initial phase to prove that the language is right, but needs to get to the part where the terminology makes sense to Joe Java. The notion of "total with remainder" is indeed confusing, and we need to find a better way to say it, but we come by it honestly.? Because the "remainder" corresponds to cases where no reasonable Java developer would want to be forced to write them out, such as "novel enum value". I think we can make things slightly (but not enough) better by using "total" for single patterns only, and using "exhaustive" for sets of patterns, since that's what drives switch exhaustiveness. But that's only a start. On 4/28/2021 2:09 PM, Maurizio Cimadamore wrote: > > I think I got the two main fallacies which led me down the wrong path: > > 1. there is a distinction between patterns that are total on T, and > patterns that are total on T, but with a "remainder" > > 2. there is a distinction between a pattern being total on T, and a > set of patterns being total (or exhaustive) on T > > This sent me completely haywire, because I was trying to reason in > terms of what a plain pattern instanceof would consider "total", and > then translate the results to nested patterns in switch - and that > didn't work, because two different sets of rules apply there. > > Maurizio > > On 28/04/2021 18:27, Brian Goetz wrote: >> I think part of the problem is that we're using the word "total" in >> different ways. >> >> A pattern P may be total on type T with remainder R.? For example, >> the pattern `Soup s` is total on `Soup` with no remainder. >> >> A _set_ of patterns may be total on T with remainder R as well.? (The >> only way a set of patterns is total is either (a) one of the patterns >> in the set is already total on T, OR (b) sealing comes into play.)? >> Maybe this should be called "exhaustive" to separate from pattern >> totality. >> >> Switch exhaustiveness derives from set-totality. >> >> Instanceof prohibits patterns that are total without remainder, for >> two reasons: (1) its silly to ask a question which constant-folds to >> `true`, and (b) the disagreement between traditional `instanceof >> Object` and `instanceof ` at null would likely be a >> source of bugs.? (This was the cost of reusing instanceof rather than >> creating a new "matches" operator.) >> >>> Foo x = ... >>> if (x instanceof Bar) >>> >>> The instanceof will not be considered total, and therefore be >>> accepted by the compiler (sorry to repeat the same question - I want >>> to make sure I understand how totality works with sealed hierarchies). >>> >> >> If the RHS of an `instanceof` is a type (not a type pattern), then >> this has traditional `instanceof` behavior.? If Bar <: Foo, then this >> is in effect a null check. >> >> If the RHS is a _pattern_, then the pattern must not be total without >> remainder.? If Bar <: Foo, `Bar b` is total on Foo, so the compiler >> says "dumb question, ask a different one." >> >> If the RHS is a non-total pattern, or a total pattern with remainder, >> then there's a real question being asked.? So in your >> Lunch-permits-Soup example, you could say >> >> ??? if (lunch instanceof Soup s) >> >> and this matches _on non-null lunch_.? Just like the switch.? The >> only difference is switch will throw on unmatched nulls, whereas >> instanceof says "no, not an instance", but that's got nothing to do >> with patterns, it's about conditional constructs. >> >>> If that's the case, I find that a bit odd - because enums kind of >>> have the same issue (but we have opted to trust that a switch on an >>> enum is total if all the constants known at compile time are >>> covered) - and, to my eyes, if you squint, a sealed hierarchy is >>> like an enum for types (e.g. sum type). >>> >> >> OK, let's talk about enums.? Suppose I have: >> >> ??? enum Lunch { Soup } >> >> and I do >> >> ??? switch (lunch) { >> ??????? case Soup -> ... >> ??? } >> >> What happens on null?? It throws, and it always has.? The behavior >> for the sealed analogue is the same; the `Soup s` pattern matches the >> non-null lunches, and if null is left unhandled elsewhere in the >> switch, the switch throws.? No asymmetry. >> >>> Anyway, backing up - this below: >>> >>> ``` >>> >>> switch (lunch) { >>> ??????? case Box(Soup s): >>> ????????????? System.err.println("Box of soup"); >>> ????????????? break; >>> >>> ??????? case Bag(Soup s): >>> ???????????? System.err.println("Bag of soup"); >>> ???????????? break; >>> >>> ??????? /* implicit */ >>> ??????? case Box(null), Bag(null): throw new NPE(); >>> ??? } >>> ``` >>> >>> is good code, which says what it means. I think the challenge will >>> be to present error messages (e.g. if the user forgot to add case >>> Box(null)) in a way that makes it clear to the user as to what's >>> missing; and maybe that will be enough. >>> >> >> The challenge here is that we don't want to force the user to handle >> the "silly" cases, such as: >> >> ??? Boolean bool = ... >> ??? switch (bool) { >> ??????? case true -> ... >> ??????? case false -> ... >> ??????? case null -> ...? // who would be happy about having to write >> this case? >> ??? } >> >> and the similarly lifted: >> >> ??? Box box = ... >> ??? switch (box) { >> ??????? case Box(true) -> ... >> ??????? case Box(false) -> ... >> ??????? case Box(null) -> ...? // who would be happy about having to >> write this case? >> ??????? case Box(novel) -> ... // or this case? >> ??????? case null ->?????????? // or this case? >> ??? } >> >> So we define the "remainder" as the values that "fall into the cracks >> between the patterns."? Users can write patterns for these, and >> they'll match, but if not, the compiler inserts code to catch these >> and throw something. >> >> The benefit is twofold: not only does the user not have to write the >> stupid cases (imagine if Box had ten slots, would we want to write >> the 2^10 partial null cases?), but because we throw on the remainder, >> DA can treat the switch as covering all boxes, and be assured there >> are no leaks. >> -------------- next part -------------- An HTML attachment was scrubbed... URL: From brian.goetz at oracle.com Wed Apr 28 20:11:49 2021 From: brian.goetz at oracle.com (Brian Goetz) Date: Wed, 28 Apr 2021 16:11:49 -0400 Subject: Switch labels (null again), some tweaking In-Reply-To: References: <9ca67b52-70a1-b0a3-7da0-da96fa5d80ab@oracle.com> <996683411.1734289.1619567042958.JavaMail.zimbra@u-pem.fr> <969484d1-ce1e-e896-1b9c-675ce79567e9@oracle.com> <76bf2826-b987-b606-271e-ad04c30d6509@oracle.com> <0906e7cf-11a3-bcff-e7cd-d922bde7e3e0@oracle.com> <0c27f925-9686-e817-e3cd-db3ff9d55fea@oracle.com> <4382a5fd-7046-29e8-d3c0-8b38ad7ab303@oracle.com> <36a7b436-251c-805c-59b3-4d153324c5af@oracle.com> <1fd988d4-9749-20fd-e281-06e0a8b3cfe3@oracle.com> <3e1df19a-1295-d69d-3ab3-bcc741623f01@oracle.com> <36846119-637e-ab86-21c3-0ec68f333111@oracle.com> <769f1882-37dc-b3fe-540f-288147a6a67b@oracle.com> Message-ID: Armed with this observation, here's a restack of the terminology and rules, but with no semantic differences from the current story, just new names. The basic idea is: ?total on T with no remainder -> total on T ?total on T with remainder R -> covers T, excepting/modulo R Concepts: ?- A pattern P can be _total_ on a type T.? Total patterns match every element of the value set of T, including null. ?- A set of patterns P* can _cover_ a type T, excepting/modulo R*.? This means that for every element in the value set of T that is not matched by some pattern in R*, it is matched by some pattern in P*. ?- If P is total on T, { P } covers T (with no exceptions.) Base cases: ?- (type patterns) The type pattern `T t` is total on all U <: T ?- (var patterns) The var pattern `var x` is total on all T ?- (default) The label `default` corresponds to a pattern that covers all types, modulo null. ?- (sealing) For an abstract sealed type S permitting T0..Tn, the set of patterns { T0, T1, ... Tn } covers S, modulo null and novel subtypes of S. Induction cases: ?- (lifting) For a deconstruction pattern D(T), { D(Q) : Q in Q* } covers D (modulo D(R*) and null) iff Q* covers T modulo R. Construct restrictions: ?- The pattern on the RHS of instanceof may not be total on the type of the LHS. ?- In a (non-legacy) switch on x : T, the set of patterns in the case labels must cover T, excepting some remainder.? It throws on the remainder. ?- In a pattern assignment `P = e`, where `e : T`, P must be cover T. It throws on the remainder. This is just a restack of the terms, but I think it eliminates the most problematic part, which is 'total with remainder'.? Now, total means total.? Total with remainder becomes "covers, excepting". On 4/28/2021 2:13 PM, Brian Goetz wrote: > I think you're right that the terminology is currently biased for > mathematicians, which is a necessary initial phase to prove that the > language is right, but needs to get to the part where the terminology > makes sense to Joe Java. > > The notion of "total with remainder" is indeed confusing, and we need > to find a better way to say it, but we come by it honestly.? Because > the "remainder" corresponds to cases where no reasonable Java > developer would want to be forced to write them out, such as "novel > enum value". > > I think we can make things slightly (but not enough) better by using > "total" for single patterns only, and using "exhaustive" for sets of > patterns, since that's what drives switch exhaustiveness. > > But that's only a start. > > On 4/28/2021 2:09 PM, Maurizio Cimadamore wrote: >> >> I think I got the two main fallacies which led me down the wrong path: >> >> 1. there is a distinction between patterns that are total on T, and >> patterns that are total on T, but with a "remainder" >> >> 2. there is a distinction between a pattern being total on T, and a >> set of patterns being total (or exhaustive) on T >> >> This sent me completely haywire, because I was trying to reason in >> terms of what a plain pattern instanceof would consider "total", and >> then translate the results to nested patterns in switch - and that >> didn't work, because two different sets of rules apply there. >> >> Maurizio >> >> On 28/04/2021 18:27, Brian Goetz wrote: >>> I think part of the problem is that we're using the word "total" in >>> different ways. >>> >>> A pattern P may be total on type T with remainder R.? For example, >>> the pattern `Soup s` is total on `Soup` with no remainder. >>> >>> A _set_ of patterns may be total on T with remainder R as well.? >>> (The only way a set of patterns is total is either (a) one of the >>> patterns in the set is already total on T, OR (b) sealing comes into >>> play.)? Maybe this should be called "exhaustive" to separate from >>> pattern totality. >>> >>> Switch exhaustiveness derives from set-totality. >>> >>> Instanceof prohibits patterns that are total without remainder, for >>> two reasons: (1) its silly to ask a question which constant-folds to >>> `true`, and (b) the disagreement between traditional `instanceof >>> Object` and `instanceof ` at null would likely be a >>> source of bugs.? (This was the cost of reusing instanceof rather >>> than creating a new "matches" operator.) >>> >>>> Foo x = ... >>>> if (x instanceof Bar) >>>> >>>> The instanceof will not be considered total, and therefore be >>>> accepted by the compiler (sorry to repeat the same question - I >>>> want to make sure I understand how totality works with sealed >>>> hierarchies). >>>> >>> >>> If the RHS of an `instanceof` is a type (not a type pattern), then >>> this has traditional `instanceof` behavior.? If Bar <: Foo, then >>> this is in effect a null check. >>> >>> If the RHS is a _pattern_, then the pattern must not be total >>> without remainder.? If Bar <: Foo, `Bar b` is total on Foo, so the >>> compiler says "dumb question, ask a different one." >>> >>> If the RHS is a non-total pattern, or a total pattern with >>> remainder, then there's a real question being asked.? So in your >>> Lunch-permits-Soup example, you could say >>> >>> ??? if (lunch instanceof Soup s) >>> >>> and this matches _on non-null lunch_.? Just like the switch.? The >>> only difference is switch will throw on unmatched nulls, whereas >>> instanceof says "no, not an instance", but that's got nothing to do >>> with patterns, it's about conditional constructs. >>> >>>> If that's the case, I find that a bit odd - because enums kind of >>>> have the same issue (but we have opted to trust that a switch on an >>>> enum is total if all the constants known at compile time are >>>> covered) - and, to my eyes, if you squint, a sealed hierarchy is >>>> like an enum for types (e.g. sum type). >>>> >>> >>> OK, let's talk about enums.? Suppose I have: >>> >>> ??? enum Lunch { Soup } >>> >>> and I do >>> >>> ??? switch (lunch) { >>> ??????? case Soup -> ... >>> ??? } >>> >>> What happens on null?? It throws, and it always has.? The behavior >>> for the sealed analogue is the same; the `Soup s` pattern matches >>> the non-null lunches, and if null is left unhandled elsewhere in the >>> switch, the switch throws.? No asymmetry. >>> >>>> Anyway, backing up - this below: >>>> >>>> ``` >>>> >>>> switch (lunch) { >>>> ??????? case Box(Soup s): >>>> ????????????? System.err.println("Box of soup"); >>>> ????????????? break; >>>> >>>> ??????? case Bag(Soup s): >>>> ???????????? System.err.println("Bag of soup"); >>>> ???????????? break; >>>> >>>> ??????? /* implicit */ >>>> ??????? case Box(null), Bag(null): throw new NPE(); >>>> ??? } >>>> ``` >>>> >>>> is good code, which says what it means. I think the challenge will >>>> be to present error messages (e.g. if the user forgot to add case >>>> Box(null)) in a way that makes it clear to the user as to what's >>>> missing; and maybe that will be enough. >>>> >>> >>> The challenge here is that we don't want to force the user to handle >>> the "silly" cases, such as: >>> >>> ??? Boolean bool = ... >>> ??? switch (bool) { >>> ??????? case true -> ... >>> ??????? case false -> ... >>> ??????? case null -> ...? // who would be happy about having to >>> write this case? >>> ??? } >>> >>> and the similarly lifted: >>> >>> ??? Box box = ... >>> ??? switch (box) { >>> ??????? case Box(true) -> ... >>> ??????? case Box(false) -> ... >>> ??????? case Box(null) -> ...? // who would be happy about having to >>> write this case? >>> ??????? case Box(novel) -> ... // or this case? >>> ??????? case null ->?????????? // or this case? >>> ??? } >>> >>> So we define the "remainder" as the values that "fall into the >>> cracks between the patterns."? Users can write patterns for these, >>> and they'll match, but if not, the compiler inserts code to catch >>> these and throw something. >>> >>> The benefit is twofold: not only does the user not have to write the >>> stupid cases (imagine if Box had ten slots, would we want to write >>> the 2^10 partial null cases?), but because we throw on the >>> remainder, DA can treat the switch as covering all boxes, and be >>> assured there are no leaks. >>> > -------------- next part -------------- An HTML attachment was scrubbed... URL: From maurizio.cimadamore at oracle.com Wed Apr 28 21:34:51 2021 From: maurizio.cimadamore at oracle.com (Maurizio Cimadamore) Date: Wed, 28 Apr 2021 22:34:51 +0100 Subject: Switch labels (null again), some tweaking In-Reply-To: References: <9ca67b52-70a1-b0a3-7da0-da96fa5d80ab@oracle.com> <996683411.1734289.1619567042958.JavaMail.zimbra@u-pem.fr> <969484d1-ce1e-e896-1b9c-675ce79567e9@oracle.com> <76bf2826-b987-b606-271e-ad04c30d6509@oracle.com> <0906e7cf-11a3-bcff-e7cd-d922bde7e3e0@oracle.com> <0c27f925-9686-e817-e3cd-db3ff9d55fea@oracle.com> <4382a5fd-7046-29e8-d3c0-8b38ad7ab303@oracle.com> <36a7b436-251c-805c-59b3-4d153324c5af@oracle.com> <1fd988d4-9749-20fd-e281-06e0a8b3cfe3@oracle.com> <3e1df19a-1295-d69d-3ab3-bcc741623f01@oracle.com> <36846119-637e-ab86-21c3-0ec68f333111@oracle.com> <769f1882-37dc-b3fe-540f-288147a6a67b@oracle.com> Message-ID: Thanks for this. It is especially helpful to see all the rules in a single place. So, armed with these rules - back to the first example I brought up: switch (lunch) { ??????? case Box(Soup s) { ???????????? if (s == null) { ????????????????? System.err.println("Box of null"); ???????????? } else { ????????????????? System.err.println("Box of soup"); ???????????? } ??????? } ??????? case Bag(Soup s): { ? ? ? ?? ??? if (s == null) { ????????????????? System.err.println("Bag of null"); ???????????? } else { ????????????????? System.err.println("Bag of soup"); ???????????? } ??????? } } If I read the rules correctly, Box(Soup) + Bag(Soup) "cover" Container, with the exception of the { null, Box(null), Bag(null) }. So the above will throw when `lunch` is null, and will also throw with Box(null) or Bag(null). Correct? So the right way to write that would be to add a couple of { case Box(null), case Bag(null) } - these will reduce the remainder of the switch blanket to just { null } - which means the switch will just throw on a null input value, as usual. Assuming I got it correct, I think that if we can get javac to spit out all the elements in the remainder (as in the list I wrote above), then it should be possible for users to understand what's going wrong, and how to fix it. Maurizio On 28/04/2021 21:11, Brian Goetz wrote: > Armed with this observation, here's a restack of the terminology and > rules, but with no semantic differences from the current story, just > new names. > > The basic idea is: > ?total on T with no remainder -> total on T > ?total on T with remainder R -> covers T, excepting/modulo R > > > Concepts: > ?- A pattern P can be _total_ on a type T.? Total patterns match every > element of the value set of T, including null. > ?- A set of patterns P* can _cover_ a type T, excepting/modulo R*.? > This means that for every element in the value set of T that is not > matched by some pattern in R*, it is matched by some pattern in P*. > ?- If P is total on T, { P } covers T (with no exceptions.) > > Base cases: > ?- (type patterns) The type pattern `T t` is total on all U <: T > ?- (var patterns) The var pattern `var x` is total on all T > ?- (default) The label `default` corresponds to a pattern that covers > all types, modulo null. > ?- (sealing) For an abstract sealed type S permitting T0..Tn, the set > of patterns { T0, T1, ... Tn } covers S, modulo null and novel > subtypes of S. > > Induction cases: > ?- (lifting) For a deconstruction pattern D(T), { D(Q) : Q in Q* } > covers D (modulo D(R*) and null) iff Q* covers T modulo R. > > > Construct restrictions: > ?- The pattern on the RHS of instanceof may not be total on the type > of the LHS. > ?- In a (non-legacy) switch on x : T, the set of patterns in the case > labels must cover T, excepting some remainder.? It throws on the > remainder. > ?- In a pattern assignment `P = e`, where `e : T`, P must be cover T. > It throws on the remainder. > > > This is just a restack of the terms, but I think it eliminates the > most problematic part, which is 'total with remainder'.? Now, total > means total.? Total with remainder becomes "covers, excepting". > > > > > On 4/28/2021 2:13 PM, Brian Goetz wrote: >> I think you're right that the terminology is currently biased for >> mathematicians, which is a necessary initial phase to prove that the >> language is right, but needs to get to the part where the terminology >> makes sense to Joe Java. >> >> The notion of "total with remainder" is indeed confusing, and we need >> to find a better way to say it, but we come by it honestly.? Because >> the "remainder" corresponds to cases where no reasonable Java >> developer would want to be forced to write them out, such as "novel >> enum value". >> >> I think we can make things slightly (but not enough) better by using >> "total" for single patterns only, and using "exhaustive" for sets of >> patterns, since that's what drives switch exhaustiveness. >> >> But that's only a start. >> >> On 4/28/2021 2:09 PM, Maurizio Cimadamore wrote: >>> >>> I think I got the two main fallacies which led me down the wrong path: >>> >>> 1. there is a distinction between patterns that are total on T, and >>> patterns that are total on T, but with a "remainder" >>> >>> 2. there is a distinction between a pattern being total on T, and a >>> set of patterns being total (or exhaustive) on T >>> >>> This sent me completely haywire, because I was trying to reason in >>> terms of what a plain pattern instanceof would consider "total", and >>> then translate the results to nested patterns in switch - and that >>> didn't work, because two different sets of rules apply there. >>> >>> Maurizio >>> >>> On 28/04/2021 18:27, Brian Goetz wrote: >>>> I think part of the problem is that we're using the word "total" in >>>> different ways. >>>> >>>> A pattern P may be total on type T with remainder R. For example, >>>> the pattern `Soup s` is total on `Soup` with no remainder. >>>> >>>> A _set_ of patterns may be total on T with remainder R as well.? >>>> (The only way a set of patterns is total is either (a) one of the >>>> patterns in the set is already total on T, OR (b) sealing comes >>>> into play.)? Maybe this should be called "exhaustive" to separate >>>> from pattern totality. >>>> >>>> Switch exhaustiveness derives from set-totality. >>>> >>>> Instanceof prohibits patterns that are total without remainder, for >>>> two reasons: (1) its silly to ask a question which constant-folds >>>> to `true`, and (b) the disagreement between traditional `instanceof >>>> Object` and `instanceof ` at null would likely be a >>>> source of bugs.? (This was the cost of reusing instanceof rather >>>> than creating a new "matches" operator.) >>>> >>>>> Foo x = ... >>>>> if (x instanceof Bar) >>>>> >>>>> The instanceof will not be considered total, and therefore be >>>>> accepted by the compiler (sorry to repeat the same question - I >>>>> want to make sure I understand how totality works with sealed >>>>> hierarchies). >>>>> >>>> >>>> If the RHS of an `instanceof` is a type (not a type pattern), then >>>> this has traditional `instanceof` behavior.? If Bar <: Foo, then >>>> this is in effect a null check. >>>> >>>> If the RHS is a _pattern_, then the pattern must not be total >>>> without remainder.? If Bar <: Foo, `Bar b` is total on Foo, so the >>>> compiler says "dumb question, ask a different one." >>>> >>>> If the RHS is a non-total pattern, or a total pattern with >>>> remainder, then there's a real question being asked.? So in your >>>> Lunch-permits-Soup example, you could say >>>> >>>> ??? if (lunch instanceof Soup s) >>>> >>>> and this matches _on non-null lunch_.? Just like the switch.? The >>>> only difference is switch will throw on unmatched nulls, whereas >>>> instanceof says "no, not an instance", but that's got nothing to do >>>> with patterns, it's about conditional constructs. >>>> >>>>> If that's the case, I find that a bit odd - because enums kind of >>>>> have the same issue (but we have opted to trust that a switch on >>>>> an enum is total if all the constants known at compile time are >>>>> covered) - and, to my eyes, if you squint, a sealed hierarchy is >>>>> like an enum for types (e.g. sum type). >>>>> >>>> >>>> OK, let's talk about enums.? Suppose I have: >>>> >>>> ??? enum Lunch { Soup } >>>> >>>> and I do >>>> >>>> ??? switch (lunch) { >>>> ??????? case Soup -> ... >>>> ??? } >>>> >>>> What happens on null?? It throws, and it always has. The behavior >>>> for the sealed analogue is the same; the `Soup s` pattern matches >>>> the non-null lunches, and if null is left unhandled elsewhere in >>>> the switch, the switch throws.? No asymmetry. >>>> >>>>> Anyway, backing up - this below: >>>>> >>>>> ``` >>>>> >>>>> switch (lunch) { >>>>> ??????? case Box(Soup s): >>>>> ????????????? System.err.println("Box of soup"); >>>>> ????????????? break; >>>>> >>>>> ??????? case Bag(Soup s): >>>>> ???????????? System.err.println("Bag of soup"); >>>>> ???????????? break; >>>>> >>>>> ??????? /* implicit */ >>>>> ??????? case Box(null), Bag(null): throw new NPE(); >>>>> ??? } >>>>> ``` >>>>> >>>>> is good code, which says what it means. I think the challenge will >>>>> be to present error messages (e.g. if the user forgot to add case >>>>> Box(null)) in a way that makes it clear to the user as to what's >>>>> missing; and maybe that will be enough. >>>>> >>>> >>>> The challenge here is that we don't want to force the user to >>>> handle the "silly" cases, such as: >>>> >>>> ??? Boolean bool = ... >>>> ??? switch (bool) { >>>> ??????? case true -> ... >>>> ??????? case false -> ... >>>> ??????? case null -> ...? // who would be happy about having to >>>> write this case? >>>> ??? } >>>> >>>> and the similarly lifted: >>>> >>>> ??? Box box = ... >>>> ??? switch (box) { >>>> ??????? case Box(true) -> ... >>>> ??????? case Box(false) -> ... >>>> ??????? case Box(null) -> ...? // who would be happy about having >>>> to write this case? >>>> ??????? case Box(novel) -> ... // or this case? >>>> ??????? case null ->?????????? // or this case? >>>> ??? } >>>> >>>> So we define the "remainder" as the values that "fall into the >>>> cracks between the patterns."? Users can write patterns for these, >>>> and they'll match, but if not, the compiler inserts code to catch >>>> these and throw something. >>>> >>>> The benefit is twofold: not only does the user not have to write >>>> the stupid cases (imagine if Box had ten slots, would we want to >>>> write the 2^10 partial null cases?), but because we throw on the >>>> remainder, DA can treat the switch as covering all boxes, and be >>>> assured there are no leaks. >>>> >> > -------------- next part -------------- An HTML attachment was scrubbed... URL: From brian.goetz at oracle.com Wed Apr 28 23:01:56 2021 From: brian.goetz at oracle.com (Brian Goetz) Date: Wed, 28 Apr 2021 19:01:56 -0400 Subject: Switch labels (null again), some tweaking In-Reply-To: References: <9ca67b52-70a1-b0a3-7da0-da96fa5d80ab@oracle.com> <996683411.1734289.1619567042958.JavaMail.zimbra@u-pem.fr> <969484d1-ce1e-e896-1b9c-675ce79567e9@oracle.com> <76bf2826-b987-b606-271e-ad04c30d6509@oracle.com> <0906e7cf-11a3-bcff-e7cd-d922bde7e3e0@oracle.com> <0c27f925-9686-e817-e3cd-db3ff9d55fea@oracle.com> <4382a5fd-7046-29e8-d3c0-8b38ad7ab303@oracle.com> <36a7b436-251c-805c-59b3-4d153324c5af@oracle.com> <1fd988d4-9749-20fd-e281-06e0a8b3cfe3@oracle.com> <3e1df19a-1295-d69d-3ab3-bcc741623f01@oracle.com> <36846119-637e-ab86-21c3-0ec68f333111@oracle.com> <769f1882-37dc-b3fe-540f-288147a6a67b@oracle.com> Message-ID: <094fde44-c408-70ee-8b9e-173833c2be20@oracle.com> On 4/28/2021 5:34 PM, Maurizio Cimadamore wrote: > > Thanks for this. > > It is especially helpful to see all the rules in a single place. > > So, armed with these rules - back to the first example I brought up: > > switch (lunch) { > ??????? case Box(Soup s) { > ???????????? if (s == null) { > ????????????????? System.err.println("Box of null"); > ???????????? } else { > ????????????????? System.err.println("Box of soup"); > ???????????? } > ??????? } > > ??????? case Bag(Soup s): { > ? ? ? ?? ??? if (s == null) { > ????????????????? System.err.println("Bag of null"); > ???????????? } else { > ????????????????? System.err.println("Bag of soup"); > ???????????? } > > ??????? } > } > > If I read the rules correctly, Box(Soup) + Bag(Soup) "cover" > Container, with the exception of the { null, Box(null), > Bag(null) }. So the above will throw when `lunch` is null, and will > also throw with Box(null) or Bag(null). Correct? > Correct (under the "we make switches total" plan.) > So the right way to write that would be to add a couple of { case > Box(null), case Bag(null) } - these will reduce the remainder of the > switch blanket to just { null } - which means the switch will just > throw on a null input value, as usual. > Correct.? Any of the following would work to capture the remainder explicitly (not all of these are valid syntax, though): ??? case null, Box(null), Bag(null):?? // explicit remainder ??? case null, Box, Bag: // basically the same ??? case null, default:?? // default misses null, so we add it in ??? case null, Container(null): // another way to say the same thing ??? case Container c:?? // total pattern, catches everything From guy.steele at oracle.com Wed Apr 28 23:06:45 2021 From: guy.steele at oracle.com (Guy Steele) Date: Wed, 28 Apr 2021 19:06:45 -0400 Subject: Switch labels (null again), some tweaking In-Reply-To: References: <9ca67b52-70a1-b0a3-7da0-da96fa5d80ab@oracle.com> <996683411.1734289.1619567042958.JavaMail.zimbra@u-pem.fr> <969484d1-ce1e-e896-1b9c-675ce79567e9@oracle.com> <76bf2826-b987-b606-271e-ad04c30d6509@oracle.com> <0906e7cf-11a3-bcff-e7cd-d922bde7e3e0@oracle.com> <0c27f925-9686-e817-e3cd-db3ff9d55fea@oracle.com> <4382a5fd-7046-29e8-d3c0-8b38ad7ab303@oracle.com> <36a7b436-251c-805c-59b3-4d153324c5af@oracle.com> <1fd988d4-9749-20fd-e281-06e0a8b3cfe3@oracle.com> <3e1df19a-1295-d69d-3ab3-bcc741623f01@oracle.com> <36846119-637e-ab86-21c3-0ec68f333111@oracle.com> <769f1882-37dc-b3fe-540f-288147a6a67b@oracle.com> Message-ID: <6567EE88-4330-4575-A106-A6FCF442245C@oracle.com> I like ?covers? better than the best phrase I came up with, which was ?ostensibly exhaustive?. :-) This looks like a good recap. ?Guy > On Apr 28, 2021, at 4:11 PM, Brian Goetz wrote: > > Armed with this observation, here's a restack of the terminology and rules, but with no semantic differences from the current story, just new names. > > The basic idea is: > total on T with no remainder -> total on T > total on T with remainder R -> covers T, excepting/modulo R > > > Concepts: > - A pattern P can be _total_ on a type T. Total patterns match every element of the value set of T, including null. > - A set of patterns P* can _cover_ a type T, excepting/modulo R*. This means that for every element in the value set of T that is not matched by some pattern in R*, it is matched by some pattern in P*. > - If P is total on T, { P } covers T (with no exceptions.) > > Base cases: > - (type patterns) The type pattern `T t` is total on all U <: T > - (var patterns) The var pattern `var x` is total on all T > - (default) The label `default` corresponds to a pattern that covers all types, modulo null. > - (sealing) For an abstract sealed type S permitting T0..Tn, the set of patterns { T0, T1, ... Tn } covers S, modulo null and novel subtypes of S. > > Induction cases: > - (lifting) For a deconstruction pattern D(T), { D(Q) : Q in Q* } covers D (modulo D(R*) and null) iff Q* covers T modulo R. > > > Construct restrictions: > - The pattern on the RHS of instanceof may not be total on the type of the LHS. > - In a (non-legacy) switch on x : T, the set of patterns in the case labels must cover T, excepting some remainder. It throws on the remainder. > - In a pattern assignment `P = e`, where `e : T`, P must be cover T. It throws on the remainder. > > > This is just a restack of the terms, but I think it eliminates the most problematic part, which is 'total with remainder'. Now, total means total. Total with remainder becomes "covers, excepting". > > > > > On 4/28/2021 2:13 PM, Brian Goetz wrote: >> I think you're right that the terminology is currently biased for mathematicians, which is a necessary initial phase to prove that the language is right, but needs to get to the part where the terminology makes sense to Joe Java. >> >> The notion of "total with remainder" is indeed confusing, and we need to find a better way to say it, but we come by it honestly. Because the "remainder" corresponds to cases where no reasonable Java developer would want to be forced to write them out, such as "novel enum value". >> >> I think we can make things slightly (but not enough) better by using "total" for single patterns only, and using "exhaustive" for sets of patterns, since that's what drives switch exhaustiveness. >> >> But that's only a start. >> >> On 4/28/2021 2:09 PM, Maurizio Cimadamore wrote: >>> I think I got the two main fallacies which led me down the wrong path: >>> >>> 1. there is a distinction between patterns that are total on T, and patterns that are total on T, but with a "remainder" >>> >>> 2. there is a distinction between a pattern being total on T, and a set of patterns being total (or exhaustive) on T >>> >>> This sent me completely haywire, because I was trying to reason in terms of what a plain pattern instanceof would consider "total", and then translate the results to nested patterns in switch - and that didn't work, because two different sets of rules apply there. >>> >>> Maurizio >>> >>> On 28/04/2021 18:27, Brian Goetz wrote: >>>> I think part of the problem is that we're using the word "total" in different ways. >>>> >>>> A pattern P may be total on type T with remainder R. For example, the pattern `Soup s` is total on `Soup` with no remainder. >>>> >>>> A _set_ of patterns may be total on T with remainder R as well. (The only way a set of patterns is total is either (a) one of the patterns in the set is already total on T, OR (b) sealing comes into play.) Maybe this should be called "exhaustive" to separate from pattern totality. >>>> >>>> Switch exhaustiveness derives from set-totality. >>>> >>>> Instanceof prohibits patterns that are total without remainder, for two reasons: (1) its silly to ask a question which constant-folds to `true`, and (b) the disagreement between traditional `instanceof Object` and `instanceof ` at null would likely be a source of bugs. (This was the cost of reusing instanceof rather than creating a new "matches" operator.) >>>> >>>>> Foo x = ... >>>>> if (x instanceof Bar) >>>>> >>>>> The instanceof will not be considered total, and therefore be accepted by the compiler (sorry to repeat the same question - I want to make sure I understand how totality works with sealed hierarchies). >>>>> >>>> >>>> If the RHS of an `instanceof` is a type (not a type pattern), then this has traditional `instanceof` behavior. If Bar <: Foo, then this is in effect a null check. >>>> >>>> If the RHS is a _pattern_, then the pattern must not be total without remainder. If Bar <: Foo, `Bar b` is total on Foo, so the compiler says "dumb question, ask a different one." >>>> >>>> If the RHS is a non-total pattern, or a total pattern with remainder, then there's a real question being asked. So in your Lunch-permits-Soup example, you could say >>>> >>>> if (lunch instanceof Soup s) >>>> >>>> and this matches _on non-null lunch_. Just like the switch. The only difference is switch will throw on unmatched nulls, whereas instanceof says "no, not an instance", but that's got nothing to do with patterns, it's about conditional constructs. >>>> >>>>> If that's the case, I find that a bit odd - because enums kind of have the same issue (but we have opted to trust that a switch on an enum is total if all the constants known at compile time are covered) - and, to my eyes, if you squint, a sealed hierarchy is like an enum for types (e.g. sum type). >>>>> >>>> >>>> OK, let's talk about enums. Suppose I have: >>>> >>>> enum Lunch { Soup } >>>> >>>> and I do >>>> >>>> switch (lunch) { >>>> case Soup -> ... >>>> } >>>> >>>> What happens on null? It throws, and it always has. The behavior for the sealed analogue is the same; the `Soup s` pattern matches the non-null lunches, and if null is left unhandled elsewhere in the switch, the switch throws. No asymmetry. >>>> >>>>> Anyway, backing up - this below: >>>>> ``` >>>>> >>>>> switch (lunch) { >>>>> case Box(Soup s): >>>>> System.err.println("Box of soup"); >>>>> break; >>>>> >>>>> case Bag(Soup s): >>>>> System.err.println("Bag of soup"); >>>>> break; >>>>> >>>>> /* implicit */ >>>>> case Box(null), Bag(null): throw new NPE(); >>>>> } >>>>> ``` >>>>> >>>>> is good code, which says what it means. I think the challenge will be to present error messages (e.g. if the user forgot to add case Box(null)) in a way that makes it clear to the user as to what's missing; and maybe that will be enough. >>>>> >>>> >>>> The challenge here is that we don't want to force the user to handle the "silly" cases, such as: >>>> >>>> Boolean bool = ... >>>> switch (bool) { >>>> case true -> ... >>>> case false -> ... >>>> case null -> ... // who would be happy about having to write this case? >>>> } >>>> >>>> and the similarly lifted: >>>> >>>> Box box = ... >>>> switch (box) { >>>> case Box(true) -> ... >>>> case Box(false) -> ... >>>> case Box(null) -> ... // who would be happy about having to write this case? >>>> case Box(novel) -> ... // or this case? >>>> case null -> // or this case? >>>> } >>>> >>>> So we define the "remainder" as the values that "fall into the cracks between the patterns." Users can write patterns for these, and they'll match, but if not, the compiler inserts code to catch these and throw something. >>>> >>>> The benefit is twofold: not only does the user not have to write the stupid cases (imagine if Box had ten slots, would we want to write the 2^10 partial null cases?), but because we throw on the remainder, DA can treat the switch as covering all boxes, and be assured there are no leaks. >>>> >>>> >> > -------------- next part -------------- An HTML attachment was scrubbed... URL: From maurizio.cimadamore at oracle.com Thu Apr 29 09:43:54 2021 From: maurizio.cimadamore at oracle.com (Maurizio Cimadamore) Date: Thu, 29 Apr 2021 10:43:54 +0100 Subject: Switch labels (null again), some tweaking In-Reply-To: <094fde44-c408-70ee-8b9e-173833c2be20@oracle.com> References: <9ca67b52-70a1-b0a3-7da0-da96fa5d80ab@oracle.com> <996683411.1734289.1619567042958.JavaMail.zimbra@u-pem.fr> <969484d1-ce1e-e896-1b9c-675ce79567e9@oracle.com> <76bf2826-b987-b606-271e-ad04c30d6509@oracle.com> <0906e7cf-11a3-bcff-e7cd-d922bde7e3e0@oracle.com> <0c27f925-9686-e817-e3cd-db3ff9d55fea@oracle.com> <4382a5fd-7046-29e8-d3c0-8b38ad7ab303@oracle.com> <36a7b436-251c-805c-59b3-4d153324c5af@oracle.com> <1fd988d4-9749-20fd-e281-06e0a8b3cfe3@oracle.com> <3e1df19a-1295-d69d-3ab3-bcc741623f01@oracle.com> <36846119-637e-ab86-21c3-0ec68f333111@oracle.com> <769f1882-37dc-b3fe-540f-288147a6a67b@oracle.com> <094fde44-c408-70ee-8b9e-173833c2be20@oracle.com> Message-ID: <635dbfaf-f5ca-a2f6-8bab-27bcd0c7465e@oracle.com> >> >> If I read the rules correctly, Box(Soup) + Bag(Soup) "cover" >> Container, with the exception of the { null, Box(null), >> Bag(null) }. So the above will throw when `lunch` is null, and will >> also throw with Box(null) or Bag(null). Correct? >> > > Correct (under the "we make switches total" plan.) I realized there's also another remainder Box/Bag(*) - e.g. Box/Bag(Pizza), where Pizza is a new type that shows up in the sealed hierarchy. To cover that (not that it's required at compile-time, just to imagine what javac would emit), we need probably one of these: case Box b: or case Container c Right? Maurizio From forax at univ-mlv.fr Thu Apr 29 12:17:07 2021 From: forax at univ-mlv.fr (forax at univ-mlv.fr) Date: Thu, 29 Apr 2021 14:17:07 +0200 (CEST) Subject: Switch labels (null again), some tweaking In-Reply-To: <635dbfaf-f5ca-a2f6-8bab-27bcd0c7465e@oracle.com> References: <36846119-637e-ab86-21c3-0ec68f333111@oracle.com> <769f1882-37dc-b3fe-540f-288147a6a67b@oracle.com> <094fde44-c408-70ee-8b9e-173833c2be20@oracle.com> <635dbfaf-f5ca-a2f6-8bab-27bcd0c7465e@oracle.com> Message-ID: <1654877613.378823.1619698627815.JavaMail.zimbra@u-pem.fr> ----- Mail original ----- > De: "Maurizio Cimadamore" > ?: "Brian Goetz" , "Remi Forax" > Cc: "amber-spec-experts" > Envoy?: Jeudi 29 Avril 2021 11:43:54 > Objet: Re: Switch labels (null again), some tweaking >>> >>> If I read the rules correctly, Box(Soup) + Bag(Soup) "cover" >>> Container, with the exception of the { null, Box(null), >>> Bag(null) }. So the above will throw when `lunch` is null, and will >>> also throw with Box(null) or Bag(null). Correct? >>> >> >> Correct (under the "we make switches total" plan.) > > I realized there's also another remainder Box/Bag(*) - e.g. > Box/Bag(Pizza), where Pizza is a new type that shows up in the sealed > hierarchy. > > To cover that (not that it's required at compile-time, just to imagine > what javac would emit), we need probably one of these: > > case Box b: > > or > > case Container c > > Right? Not necessarily. First, let's acknowledge that you are moving the discussion from the basic semantics to talk about the translation strategy which touch more the binary compatibility, aka, what is the semantics when the view of the world at compile time and at runtime are disagreeing. There are at least two reasons to let javac emits such cases, first like with null as remainder, you have to add a lot of cases once you have sub-patterns, so it will bloat the bytecode, then if we insert such cases, it means that we also have a code that throw a specific exception bolt into the bytecode which means that we can not change the binary compatibility story later. The jobs to implement the binary compatibility semantics is the job of the runtime not the compiler, like with lambdas, where the LambdaMetaFactory does some runtime checks before generating + linking the lambda proxy, I expect a SwitchMetafactory to do the same job. This is more flexible and allows the runtime to be updated when the VM change by example, the lambdaMetafactory code was changed with the introduction of hidden classes. Now, the big question is what binary compatibility semantics we want ? We can by example throw a subclass of LinkageError upfront during the linking when we detect that the sealed type as more subclasses that the one accessible at compile time, but it means that adding a new subclass to a sealed type is not a binary compatible change anymore. We can delay the detection the an incompatible class until we see one at runtime, so adding a new subclass is a compatible change but calling a switch with an instance of such subclass is not. There is also the question about the order of the cases, should the order of the cases be validated at runtime or not, by example, if we have B < A at compile time but A < B at runtime with a switch that like case B first and case A second. > > Maurizio R?mi From maurizio.cimadamore at oracle.com Thu Apr 29 12:21:53 2021 From: maurizio.cimadamore at oracle.com (Maurizio Cimadamore) Date: Thu, 29 Apr 2021 13:21:53 +0100 Subject: Switch labels (null again), some tweaking In-Reply-To: <1654877613.378823.1619698627815.JavaMail.zimbra@u-pem.fr> References: <36846119-637e-ab86-21c3-0ec68f333111@oracle.com> <769f1882-37dc-b3fe-540f-288147a6a67b@oracle.com> <094fde44-c408-70ee-8b9e-173833c2be20@oracle.com> <635dbfaf-f5ca-a2f6-8bab-27bcd0c7465e@oracle.com> <1654877613.378823.1619698627815.JavaMail.zimbra@u-pem.fr> Message-ID: <2f9b8cb9-d7af-ccf7-9248-fe7daa2a6c63@oracle.com> On 29/04/2021 13:17, forax at univ-mlv.fr wrote: > Not necessarily. > > First, let's acknowledge that you are moving the discussion from the basic semantics to talk about the translation strategy which touch more the binary compatibility, > aka, what is the semantics when the view of the world at compile time and at runtime are disagreeing. I wasn't necessarily yet talking about translation strategy - as much I was trying to understand what a user could do to "bullet proof" its switches. It seems to me that there's a way to name almost all the components of a remainder, except for the "unknown" ones (which come from sealed hiearchies). I'm not (yet) concerned with bytecode considerations :-) Maurizio From brian.goetz at oracle.com Thu Apr 29 14:46:23 2021 From: brian.goetz at oracle.com (Brian Goetz) Date: Thu, 29 Apr 2021 10:46:23 -0400 Subject: Switch labels (null again), some tweaking In-Reply-To: <635dbfaf-f5ca-a2f6-8bab-27bcd0c7465e@oracle.com> References: <996683411.1734289.1619567042958.JavaMail.zimbra@u-pem.fr> <969484d1-ce1e-e896-1b9c-675ce79567e9@oracle.com> <76bf2826-b987-b606-271e-ad04c30d6509@oracle.com> <0906e7cf-11a3-bcff-e7cd-d922bde7e3e0@oracle.com> <0c27f925-9686-e817-e3cd-db3ff9d55fea@oracle.com> <4382a5fd-7046-29e8-d3c0-8b38ad7ab303@oracle.com> <36a7b436-251c-805c-59b3-4d153324c5af@oracle.com> <1fd988d4-9749-20fd-e281-06e0a8b3cfe3@oracle.com> <3e1df19a-1295-d69d-3ab3-bcc741623f01@oracle.com> <36846119-637e-ab86-21c3-0ec68f333111@oracle.com> <769f1882-37dc-b3fe-540f-288147a6a67b@oracle.com> <094fde44-c408-70ee-8b9e-173833c2be20@oracle.com> <635dbfaf-f5ca-a2f6-8bab-27bcd0c7465e@oracle.com> Message-ID: <5462ca77-44b5-9196-7744-53d33a1f25c4@oracle.com> Whenever you have a deconstruction pattern, there's always a remainder null. (Point(var x, var y) NPEs on null.) Whenever you cover a sealed type with alternatives (but no total pattern), there's always remainder null and "novel". Whenever you lift over a nestable pattern (like D(T), or String[] { P }), you have to wrap the remainder with the nestable thing. So yes, Container(Pizza) is also remainder. Based on historical precedent, its tempting to specify NPE for null remainder and ICCE for novel remainder.? (I can also imagine cases where the logical exception type is CCE.)? But that doesn't scale up very well; what do we throw for DoubleLunchPack(null, pizza)?? It has both null- and novel-derived remainders.? I don't think we want the compiler generating tons of code just so each remainder can get a different exception; more likely, the compiler will want to generate some sort of "default: throwSomethingSimple" clause. On 4/29/2021 5:43 AM, Maurizio Cimadamore wrote: > >>> >>> If I read the rules correctly, Box(Soup) + Bag(Soup) "cover" >>> Container, with the exception of the { null, Box(null), >>> Bag(null) }. So the above will throw when `lunch` is null, and will >>> also throw with Box(null) or Bag(null). Correct? >>> >> >> Correct (under the "we make switches total" plan.) > > I realized there's also another remainder Box/Bag(*) - e.g. > Box/Bag(Pizza), where Pizza is a new type that shows up in the sealed > hierarchy. > > To cover that (not that it's required at compile-time, just to imagine > what javac would emit), we need probably one of these: > > case Box b: > > or > > case Container c > > Right? > > Maurizio > > -------------- next part -------------- An HTML attachment was scrubbed... URL: From maurizio.cimadamore at oracle.com Thu Apr 29 14:48:40 2021 From: maurizio.cimadamore at oracle.com (Maurizio Cimadamore) Date: Thu, 29 Apr 2021 15:48:40 +0100 Subject: Switch labels (null again), some tweaking In-Reply-To: <5462ca77-44b5-9196-7744-53d33a1f25c4@oracle.com> References: <996683411.1734289.1619567042958.JavaMail.zimbra@u-pem.fr> <969484d1-ce1e-e896-1b9c-675ce79567e9@oracle.com> <76bf2826-b987-b606-271e-ad04c30d6509@oracle.com> <0906e7cf-11a3-bcff-e7cd-d922bde7e3e0@oracle.com> <0c27f925-9686-e817-e3cd-db3ff9d55fea@oracle.com> <4382a5fd-7046-29e8-d3c0-8b38ad7ab303@oracle.com> <36a7b436-251c-805c-59b3-4d153324c5af@oracle.com> <1fd988d4-9749-20fd-e281-06e0a8b3cfe3@oracle.com> <3e1df19a-1295-d69d-3ab3-bcc741623f01@oracle.com> <36846119-637e-ab86-21c3-0ec68f333111@oracle.com> <769f1882-37dc-b3fe-540f-288147a6a67b@oracle.com> <094fde44-c408-70ee-8b9e-173833c2be20@oracle.com> <635dbfaf-f5ca-a2f6-8bab-27bcd0c7465e@oracle.com> <5462ca77-44b5-9196-7744-53d33a1f25c4@oracle.com> Message-ID: <0cd62502-44ea-dc87-879c-2425be12eb18@oracle.com> On 29/04/2021 15:46, Brian Goetz wrote: > I don't think we want the compiler generating tons of code just so > each remainder can get a different exception; more likely, the > compiler will want to generate some sort of "default: > throwSomethingSimple" clause. Yeah - that's my understanding, as far as code generation goes. I guess an interesting question, for when we generate better error messages to show remainder, would be how to "spell" (e.g. in the diagnostic) those remainders associated with not-yet known types. But that's a question for another day. Maurizio -------------- next part -------------- An HTML attachment was scrubbed... URL: From brian.goetz at oracle.com Thu Apr 29 14:52:28 2021 From: brian.goetz at oracle.com (Brian Goetz) Date: Thu, 29 Apr 2021 10:52:28 -0400 Subject: Switch labels (null again), some tweaking In-Reply-To: <1654877613.378823.1619698627815.JavaMail.zimbra@u-pem.fr> References: <36846119-637e-ab86-21c3-0ec68f333111@oracle.com> <769f1882-37dc-b3fe-540f-288147a6a67b@oracle.com> <094fde44-c408-70ee-8b9e-173833c2be20@oracle.com> <635dbfaf-f5ca-a2f6-8bab-27bcd0c7465e@oracle.com> <1654877613.378823.1619698627815.JavaMail.zimbra@u-pem.fr> Message-ID: Without diving into the translation details prematurely, I just want to note that we *do* want the compiler to "totalize" things for us, because, DA.? Suppose we have A permits B, C: ??? int x ; ??? switch (a) { ??????? case B(int foo): x = foo; break; ??????? case C(int bar): x = bar; break; ??? } ??? // HERE There is some remainder -- null, D -- but we don't want that remainder to leak past the switch; when we get to HERE, we'd like x to be DA, and a to have matched one of the patterns.? So the compiler has to insert _something_ to handle the remaining cases, even if it is only: ??? default: throw new TotalityException("blah blah"); But, the details of code generation are not what M is asking about now -- he's asking "if I were the user, could I write a truly covering switch?"? And the answer is yes, but the shape of the remainder is "spiny" and can get complicated quickly. > Not necessarily. > > First, let's acknowledge that you are moving the discussion from the basic semantics to talk about the translation strategy which touch more the binary compatibility, > aka, what is the semantics when the view of the world at compile time and at runtime are disagreeing. > > There are at least two reasons to let javac emits such cases, > first like with null as remainder, you have to add a lot of cases once you have sub-patterns, so it will bloat the bytecode, > then if we insert such cases, it means that we also have a code that throw a specific exception bolt into the bytecode which means that we can not change the binary compatibility story later. > > The jobs to implement the binary compatibility semantics is the job of the runtime not the compiler, like with lambdas, where the LambdaMetaFactory does some runtime checks before generating + linking the lambda proxy, > I expect a SwitchMetafactory to do the same job. This is more flexible and allows the runtime to be updated when the VM change by example, the lambdaMetafactory code was changed with the introduction of hidden classes. > > Now, the big question is what binary compatibility semantics we want ? > > We can by example throw a subclass of LinkageError upfront during the linking when we detect that the sealed type as more subclasses that the one accessible at compile time, but it means that adding a new subclass to a sealed type is not a binary compatible change anymore. We can delay the detection the an incompatible class until we see one at runtime, so adding a new subclass is a compatible change but calling a switch with an instance of such subclass is not. > > There is also the question about the order of the cases, should the order of the cases be validated at runtime or not, by example, if we have B < A at compile time but A < B at runtime with a switch that like case B first and case A second. > >> Maurizio > R?mi From brian.goetz at oracle.com Thu Apr 29 14:54:43 2021 From: brian.goetz at oracle.com (Brian Goetz) Date: Thu, 29 Apr 2021 10:54:43 -0400 Subject: Switch labels (null again), some tweaking In-Reply-To: <0cd62502-44ea-dc87-879c-2425be12eb18@oracle.com> References: <969484d1-ce1e-e896-1b9c-675ce79567e9@oracle.com> <76bf2826-b987-b606-271e-ad04c30d6509@oracle.com> <0906e7cf-11a3-bcff-e7cd-d922bde7e3e0@oracle.com> <0c27f925-9686-e817-e3cd-db3ff9d55fea@oracle.com> <4382a5fd-7046-29e8-d3c0-8b38ad7ab303@oracle.com> <36a7b436-251c-805c-59b3-4d153324c5af@oracle.com> <1fd988d4-9749-20fd-e281-06e0a8b3cfe3@oracle.com> <3e1df19a-1295-d69d-3ab3-bcc741623f01@oracle.com> <36846119-637e-ab86-21c3-0ec68f333111@oracle.com> <769f1882-37dc-b3fe-540f-288147a6a67b@oracle.com> <094fde44-c408-70ee-8b9e-173833c2be20@oracle.com> <635dbfaf-f5ca-a2f6-8bab-27bcd0c7465e@oracle.com> <5462ca77-44b5-9196-7744-53d33a1f25c4@oracle.com> <0cd62502-44ea-dc87-879c-2425be12eb18@oracle.com> Message-ID: <69d42b54-4fb7-f277-2773-d3c0fdf4cdb8@oracle.com> Indeed.? It's not clear how much work it is worth doing to discover that the reason _why_ we're in the remainder is that we had a pattern: ??? case Foo(Bar(Baz(Mumble(Soup s)))): and someone passed us a Foo(Bar(Baz(Mumble(pizza)))). We could statically emit a description of the remainder, but it would get large and esoteric quickly if pattern nesting got "deep" or "wide".? Seems mostly like an ugly secret the compiler should keep to itself. On 4/29/2021 10:48 AM, Maurizio Cimadamore wrote: > > > On 29/04/2021 15:46, Brian Goetz wrote: >> I don't think we want the compiler generating tons of code just so >> each remainder can get a different exception; more likely, the >> compiler will want to generate some sort of "default: >> throwSomethingSimple" clause. > > Yeah - that's my understanding, as far as code generation goes. > > I guess an interesting question, for when we generate better error > messages to show remainder, would be how to "spell" (e.g. in the > diagnostic) those remainders associated with not-yet known types. > > But that's a question for another day. > > Maurizio > -------------- next part -------------- An HTML attachment was scrubbed... URL: From forax at univ-mlv.fr Thu Apr 29 15:19:48 2021 From: forax at univ-mlv.fr (forax at univ-mlv.fr) Date: Thu, 29 Apr 2021 17:19:48 +0200 (CEST) Subject: Switch labels (null again), some tweaking In-Reply-To: References: <094fde44-c408-70ee-8b9e-173833c2be20@oracle.com> <635dbfaf-f5ca-a2f6-8bab-27bcd0c7465e@oracle.com> <1654877613.378823.1619698627815.JavaMail.zimbra@u-pem.fr> Message-ID: <1988024594.540082.1619709588946.JavaMail.zimbra@u-pem.fr> ----- Mail original ----- > De: "Brian Goetz" > ?: "Remi Forax" , "Maurizio Cimadamore" > Cc: "amber-spec-experts" > Envoy?: Jeudi 29 Avril 2021 16:52:28 > Objet: Re: Switch labels (null again), some tweaking > Without diving into the translation details prematurely, I just want to > note that we *do* want the compiler to "totalize" things for us, > because, DA.? Suppose we have A permits B, C: > > ??? int x ; > ??? switch (a) { > ??????? case B(int foo): x = foo; break; > ??????? case C(int bar): x = bar; break; > ??? } > ??? // HERE > > There is some remainder -- null, D -- but we don't want that remainder > to leak past the switch; when we get to HERE, we'd like x to be DA, and > a to have matched one of the patterns.? So the compiler has to insert > _something_ to handle the remaining cases, even if it is only: > > ??? default: throw new TotalityException("blah blah"); > Not necessarily, the synthetic switch can use default for the last case int x; var carrier = invokedynamic (a); switch(carrier.dispatch) { case 0: int foo = carrier.foo(); x = foo; break; default: int bar = carrier.bar(); x = bar; break; } and let the runtime part bound to invokedynamic to take care of the remainder. > But, the details of code generation are not what M is asking about now > -- he's asking "if I were the user, could I write a truly covering > switch?"? And the answer is yes, but the shape of the remainder is > "spiny" and can get complicated quickly. Ok R?mi > >> Not necessarily. >> >> First, let's acknowledge that you are moving the discussion from the basic >> semantics to talk about the translation strategy which touch more the binary >> compatibility, >> aka, what is the semantics when the view of the world at compile time and at >> runtime are disagreeing. >> >> There are at least two reasons to let javac emits such cases, >> first like with null as remainder, you have to add a lot of cases once you have >> sub-patterns, so it will bloat the bytecode, >> then if we insert such cases, it means that we also have a code that throw a >> specific exception bolt into the bytecode which means that we can not change >> the binary compatibility story later. >> >> The jobs to implement the binary compatibility semantics is the job of the >> runtime not the compiler, like with lambdas, where the LambdaMetaFactory does >> some runtime checks before generating + linking the lambda proxy, >> I expect a SwitchMetafactory to do the same job. This is more flexible and >> allows the runtime to be updated when the VM change by example, the >> lambdaMetafactory code was changed with the introduction of hidden classes. >> >> Now, the big question is what binary compatibility semantics we want ? >> >> We can by example throw a subclass of LinkageError upfront during the linking >> when we detect that the sealed type as more subclasses that the one accessible >> at compile time, but it means that adding a new subclass to a sealed type is >> not a binary compatible change anymore. We can delay the detection the an >> incompatible class until we see one at runtime, so adding a new subclass is a >> compatible change but calling a switch with an instance of such subclass is >> not. >> >> There is also the question about the order of the cases, should the order of the >> cases be validated at runtime or not, by example, if we have B < A at compile >> time but A < B at runtime with a switch that like case B first and case A >> second. >> >>> Maurizio > > R?mi From john.r.rose at oracle.com Fri Apr 30 05:31:38 2021 From: john.r.rose at oracle.com (John Rose) Date: Fri, 30 Apr 2021 05:31:38 +0000 Subject: Switch labels (null again), some tweaking In-Reply-To: <1988024594.540082.1619709588946.JavaMail.zimbra@u-pem.fr> References: <094fde44-c408-70ee-8b9e-173833c2be20@oracle.com> <635dbfaf-f5ca-a2f6-8bab-27bcd0c7465e@oracle.com> <1654877613.378823.1619698627815.JavaMail.zimbra@u-pem.fr> <1988024594.540082.1619709588946.JavaMail.zimbra@u-pem.fr> Message-ID: <178965FE-4F40-4364-91FA-B3BC198B90A7@oracle.com> On Apr 29, 2021, at 8:19 AM, forax at univ-mlv.fr wrote: and let the runtime part bound to invokedynamic to take care of the remainder. +100 and another +100 for the term-of-art ?spiny? -------------- next part -------------- An HTML attachment was scrubbed... URL: