Continue in switch

John Rose john.r.rose at oracle.com
Tue May 8 20:31:12 UTC 2018


On May 8, 2018, at 12:31 PM, Brian Goetz <brian.goetz at oracle.com> wrote:
> 
> If continue is the right notion, our choice is essentially between:
>  - drive towards continue in switch working like it does in everything else, and deal with ambiguities (switch in loop) as they come up;
>  - drive towards something that looks sufficiently different from "naked" continue (continue with label, "continue switch") in switches, and accept there will be a permanent seam (in switch you do it this way, in loops, this other way.)

(Cards on table:  I'm on Team `continue switch;` and $0.02 follows.)

The permanent seam doesn't bother me very much, for the following
explainable reason:  Switch is not a proper loop, although it is a
continuable statement.  If you want to continue a continuable
statement that isn't a loop, you have to be specific.  (And you
should be specific anyway if your code is deeply nested.)

Classic Java loops while/for/do-while are (usually) not statically
bounded.  Depending on their logic they can run an arbitrary
number of times.

Now, by saying "continue" in a switch, we are ascribing it a loopish
nature.  And this is true, because a switch is (in our new theory)
a _decision chain_, that is a finite, sequence of statically defined
predicates which are tested in order until one matches.  The number
of predicates is arbitrary (like a loop) but they are listed statically
(unlike a loop).

In both loops and decision chains there is a well-defined and very
useful control flow operation, which is "continue to the next step".
The next step of the loop is to run the iteration and execute the
loop body again.  The next step of the decision chain is to abandon
the current predicate and test the next one.

The "continue" concept fits the bill for both.  But compatibility says
a bare "continue" must go to a proper loop, not a decision chain.
Thus, bare continue always reaches the enclosing proper loop,
but "continue switch" or "continue L" (L labeling the switch)
reaches the enclosing switch, because although switches are
not proper loops, they are loopish, they are continuable even
if they cannot run forever like a loop.

Explained this way, the "seam" Brian is referring to is an artifact,
not of history, but of the distinction between proper loops and
other continuable ("loopish") constructs.  Personally I'd be fine
with this seam as a permanent thing.


The seam can be reduced also, and I think that is what Brian
is aiming at.  Bare "continue" in an expression switch will always be
unambiguous, since it *cannot* reach an enclosing loop (no branches
out from an expression.)  Sailing yet more closely to the wind:  Bare
"continue" in a switch *not* nested in a loop is also unambiguous,
since there's no proper loop to reach.

Brian, are you thinking that bare continue, inside switch, *inside loop*,
is an *ambiguity error*?  That would be worth warning about about:
Today's correct code would become an error tomorrow:

for (;;) {
   switch (x) {
      case 0:
        continue;  // Error/warning: ambiguous unlabeled continue
   }
}

The message could say "unlabeled continue is ambiguous when
nested in both switch and proper loop, repair by saying either
'continue switch' or 'continue for', or use a label."

In that case, the warnings could be sent even after feature adoption.
Eventually when the warnings turn to errors, no code changes
semantics, but some code breaks.  Or make it be a warning forever.


OK, now for some "decision chain theory".

Besides switches, decision chains have two other forms which are
worth contemplating, as elucidating the essential structure in another
surface form, and also as a possible refactoring target.

switch (x) {
 case P1 -> S1;  // suppressing legacy fallthrough for simplicity
 case P2 -> S2;
 …
}

if (x matches P1) S1;
else if (x matches P2)  S2;
else …

for (Function<XType, Optional<YType>> casef : List.of(
   x -> x matches P1 ? Optional.of(S1) : Optional.none(),
   x -> x matches P2 ? Optional.of(S2) : Optional.none()
   )) {
  var y = casef.apply(x);
  if (!y.isPresent())   continue;
  result = y.get();
  break;
}

The third form seems exotic but it is a coding pattern some
of us have surely used for decision chains, test lists, and
on similar occasions.  (I have!)

The continue keyword is native in the third form, and corresponds
exactly to the proposed continue [switch] form for the first form.

OK, now I'm going to push farther, by your leave, with a thought
experiment exploring and extending the correspondence between
the first two forms of decision chain, switch and if/else.

The second form is of course a far more common refactoring of
decision chains; in fact one of the motivations of pattern-switch
is to refactor many existing if/else decision chains into easier-to-read
switches.  Here's an example:

if (x matches Plus(0.0, var a)) {
   res = a;
} else if (x matches Plus(var a, 0.0)) {
   res = a;
} else {
   res = x;
}

This simplifies to a switch-based decision chain:

switch (x) {
case Plus(0.0, var a) -> res = a;
case Plus(var a, 0.0) -> res = a;
default -> res = x;
}

Here's the odd part:  The close duality between if and switch forms
suggests that we should also consider "continue if", as a form which
means "find the innermost enclosing 'if' with a continuation point
and branch to it".  What on earth is a continuation point of an 'if'?
That's easy, it's spelled "else".  In other words, if/else (not just
if w/o else) is loopish too.  (And "if" without an "else" gets passed
by from "continue" since there is no continuation point.)

Here's the same example, but enhanced with guards (isNan):

switch (x) {
case Plus(0.0, var a):
   if (a.isNan())  continue switch;
   res = a;
   break;
case Plus(var a, 0.0):
   if (a.isNan())  continue switch;
   res = a;
   break;
default:
  res = x;
}

What's the equivalent if/else decision chain, with the same guards?

if (x matches Plus(0.0, var a) && !a.isNan()) {
   res = a;
} else if (x matches Plus(var a, 0.0) && !a.isNan()) {
   res = a;
} else {
   res = x;
}

But this one pushes the guards to one side, making the
basic structure easier to read (in some cases, not all!):

if (x matches Plus(0.0, var a)) {
   if (a.isNan())  continue if;
   res = a;
} else if (x matches Plus(var a, 0.0)) {
   if (a.isNan())  continue if;
   res = a;
} else {
   res = x;
}

How often have you added a complicated "&& !foo" rider expression
to an otherwise clear "if/else" chain, just to push control forward towards
the next "else" which miight handle your marginal "foo" condition?
I have, many times.  I think "continue if" would have been helpful.

(Note that "break if" is slightly useful too, if you just want to execute
a localized action for some marginal condition and be done.  Today
you need to nest your main action equally with the marginal action,
putting an if/else inside the if/else.  That's not too bad, but we might
sometime might prefer the option of making the marginal case be
an asymmetrical add-on to the main flow of logic.)

— John

-------------- next part --------------
An HTML attachment was scrubbed...
URL: <http://mail.openjdk.java.net/pipermail/amber-spec-experts/attachments/20180508/aabd6a11/attachment-0001.html>


More information about the amber-spec-experts mailing list