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 <brian.go...@oracle.com> 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 <total pattern>` 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<Boolean> 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. >>>> >>>> >>>> >> >