> On Aug 24, 2020, at 1:30 PM, Brian Goetz <[email protected]> wrote:
>
> The previous mail, on optimistic totality, only applied to switches that were
> already total. Currently that includes only expression switches; we want a
> non-too-invasive way to mark a statement switch as total as well. . . .
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. In this essay I will first tease these two issues apart, and then
suggest how we might go forward using what we have learned from discussions of
the last few weeks.
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”.
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.
So, standing where we are today, first imagine that we relax the totality
requirement of switch expressions and allow them to produce default values
(zero or null) in exactly the same situation that a statement switch would “do
nothing”.
Next, let us introduce pattern matching in switch labels, as we have discussed
at length.
Then we introduce two mechanisms that we have discussed more recently, and say
that each of these mechanisms may be used in either a switch statement or a
switch expression.
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.
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.
In this way two distinct methods are provided for requesting totality checking
(and note that they are mutually exclusive), and either may be used with either
a switch statement switch or a switch expression.
At this stage, we have six possibilities, generated by an _orthogonal_ choice
of (1) statement versus expression, and (2) use of “default <pattern>”, “switch
case”, or neither.
But we are still justly worried that _one_ of these _six_ cases is error-prone:
the possibility of switch expressions generating default values. So we can
rule that out again, but in a more principled way that still retains both
orthogonality of choice and backward compatibility. We replace this line of the
JLS:
• If the type of the selector expression is not an enum type, then
there is exactly one default label associated with the switch block.
with this:
• 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.
Furthermore, we retain the existing sentence in the description of the run-time
evaluation of switch expressions that says "If no switch label matches, then an
IncompatibleClassChangeError is thrown and the entire switch expression
completes abruptly for that reason.”
In this way we have six orthogonally generated choices (instead of two
non-orthogonally-generated possibilities), of which we then, for the sake of
backward compatibility, allow the most dangerous one to be used only for enums,
and add back the previously existing ICCE guardrail for that situation, so that
switch expressions never generate default values after all.