> On Aug 24, 2020, at 5:04 PM, Brian Goetz <brian.go...@oracle.com> wrote:
>
>
>
>> I am going to argue here that, just as fear of letting nulls flow stemmed
>> from a early design that conflated multiple design issues as a result of
>> extrapolating from too few data points (enums and strings), we have been
>> boxed into another corner because we conflated expression-ness and the need
>> for totality.
>
> I'm not sure we _conflated_ the two, as we did this with our eyes open (and
> fairly recently), but I suspect I agree with the rest -- that for $REASONS,
> we introduced an asymmetry that we knew would come back to bite us, and left
> a note for ourselves to come back and revisit, especially as optimistic
> totality became more important (e.g., through sealed types.)
>
>> Going back to the dawn of time, a switch statement does not have to be
>> total. Why is this possible? Because there is an obvious default behavior:
>> do nothing. If we were to view it in terms of delivering a value of some
>> type, we would say that type is “void”.
>
> Yep. Cue usual comparison to "if without else." Partiality, for statements,
> is OK, but not for expressions. Can't have a ternary expression with no `:
> alternative` part.
>
>> Then why did we not allow a switch expression to be _exactly_ analogous? In
>> fact, we could have, by relying on existing precedent in the language: if no
>> switch label matches and there is no default, or if execution of the
>> statements of the switch block completes normally, we could simply decree
>> that a switch expression has the default behavior “do nothing” and delivers
>> a _default value_—exactly as we do for initialization of fields and array
>> components. So for
>>
>> enum Color { RED, GREEN, BLUE }
>> Color x = …
>> int n = switch (x) { RED -> 1; GREEN -> 2; };
>>
>> then if x is BLUE, n will get the value 0.
>>
>> But I am guessing that we worried about programming errors and demanded
>> totality for switch expressions, so we enforced it by fiat because we had no
>> other mechanism to request totality.
>
> Yes, that's right. Note that this is analogous to another feature that is
> frequently requested -- the so-called "safe dereference" operators, where
> `x?.y` yields a default value if `x` is null. When this one first reared its
> head (and about a thousand times since, since it comes up near-constantly),
> our objection was that, while `null` might conceivably be a reasonable
> default for such an expression if `y` is of reference type, yielding a
> default primitive value is more likely to lead to errors than not. (This is
> not unlike the problem with `Map::get`, where `null` means both "mapping not
> present" and "element mapped to null", and there's no way to tell the
> difference unless you can freeze the map for updates while asking two
> questions (Map::containsKey and Map::get.) The argument against both is the
> same.
>
>> The first is a switch label of the form “default <pattern>”, which behaves
>> just like a switch label “case <pattern>” except that it is a static error
>> if the <pattern> is not total on the type of the selector expression. This
>> mechanism is good for extensible type hierarchies, where we expect to call
>> out a number of special cases and then have a catch-all case, and we want
>> the compiler to confirm to us on every compilation that the catch-all case
>> actually does catch everything.
>
> What I like about this use of default is that it decomposes into independent
> parts. The `default` case means "everything else"; `default
> <total-pattern>` means "everything else, and destructure that everything else
> with this pattern, which had better be total because, well, I'm matching
> everything else." THe addition of a pattern doesn't change the meaning
> of default; it is a form of composition. You could recast this as taking one
> feature -- "patterns in switch" -- and turning it into two: patterns in case
> labels, and patterns in default labels.
>
>> The second is the possibility of writing “switch case” rather than “switch”,
>> which introduces these extra constraints on the switch block: It is a static
>> error if any SwitchLabel of the switch statement begins with “default". It
>> is a static error if the set of case patterns is not at least optimistically
>> total on the type of the selector expression. It is a static error if the
>> last BlockStatement in _any_ SwitchBlockStatementGroup, or the Block in any
>> SwitchRule, can complete normally. It is a static error if any SwitchLabel
>> of the switch statement is not part of a SwitchBlockStatementGroup or
>> SwitchRule. In addition, the compiler automatically inserts
>> SwitchBlockStatementGroups or SwitchRules to cover the residue, so as to
>> throw an appropriate error at run time if the value produced by the selector
>> expression belongs to the residue. This mechanism is good for enums and
>> sealed types, that is, situations where we expect to enumerate all the
>> special cases explicitly and want to be notified by the compiler (or failing
>> that, at run time) if we have failed to do so.
>
> Is it fair to recharacterize this as follows -- we divide potentially-total
> statement switches into two kinds:
> - Those that arrive at totality via a catch-all clause;
> - Those that arrive at totality via an (optimistic) covering of an
> enumerated domain ("switch by parts")
>
> and we provide a separate mechanism for each to declare their totality (which
> engages different type checking and translation.)
>
> (I see later that you say this, so good, we're on the same page.)
>
> (We might have called this "enum switch", especially if we had taken Alan's
> suggestion of declaring sealed types with a more enum-like syntax.)
>
> <digression>
> Some comments on the restrictions:
>
>> It is a static error if the last BlockStatement in _any_
>> SwitchBlockStatementGroup, or the Block in any SwitchRule, can complete
>> normally.
>
> This is mostly already true in general; we envision it is a static error if
> you fall into, or out of, any case that has bindings. (We might relax this
> to allow falling _into_ a total case.) This was a simplification; we could
> support binding merging, but the return-on-complexity didn't seem quite there.
>
> </digression>
>
>> • If the type of the selector expression is not an enum type, then either
>> the “switch case” form is used or there is exactly one default label
>> associated with the switch block.
>
> I presume you intend that this eventually becomes true for switches on sealed
> types as well.
Yes, sorry, I didn’t state the conditions quite correctly, and I believe the
correct way to state them will emerge once we work out the complete theory of
optimistic totality.
> One question I have is that optimistic totality can apply in switches that
> are not on sealed types, or where sealed types show up in nested contexts.
> For example:
>
> switch (box) {
> case Box(var x): ...
> }
>
> Here, I think we want to say that Box(var x) is o.t. on Box, but it doesn't
> match null. So how does the programmer indicate that they want to get
> totality checking and residue rejection?
I believe that “switch case” can handle this:
switch case (box) {
case Box(var x): ...
}
This says, among other things, that it is a static error if the (singleton) set
of case patterns { Box(var x) } is not o.t. on the type of “box”, and it says
we want residue checking, so it’s as if the compiler rewrote it to:
switch case (box) {
case null: throw <null residue error, which could be NPE or something
else>
case Box(var x): ...
}
Alternatively, we could write
switch (box) {
default Box(var x): ...
}
which says that it is a static error if the pattern Box(var x) is not total on
the type of “box”. It’s not, because it doesn’t match null, so we get a static
error, as desired. Perhaps we should have written
switch (box) {
case Box(var x): …
default Box z: ...
}
But I’m thinking the “switch case” solution is preferable for this specific
example.
> Similarly, suppose we have a sealed type Shape = Circ + Rect, and the obvious
> container Box<Shape>:
>
> switch (boxOfShape) {
> case Box(Circ c):
> case Box(Rect r):
> }
>
> Again, I think we want this set of cases to be o.t., but the switch is not on
> a sealed type. I am not sure how to integrate these cases into your model.
And again, I think that “switch case” does the job:
switch case (boxOfShape) {
case Box(Circ c): ...
case Box(Rect r): ...
}
This says, among other things, that it is a static error if the set of case
patterns { Box(Circ c), Box(Rest r) } is not o.t. on the type of “boxOfShape”,
and it says we want residue checking, so it’s as if the compiler rewrote it to:
switch case (boxOfShape) {
case null: throw <null residue error, which could be NPE or something
else>
case Box(Circ c): ...
case Box(Rect r): …
case Box(var _): throw <unknown residue error, which could be ICCE or
something else>
}