Expression switch feedback

Brian Goetz brian.goetz at oracle.com
Sun Apr 8 14:49:13 UTC 2018


> The model being used for expression switches is that they are just
> switches with a few enhancements. I'm uncomfortable with this for a
> number of reasons.

If you weren't uncomfortable, you'd not be paying attention.  The 
tradeoffs and constraints here are significant.

> 1) Personally I find the following to be deep flaws in statement switch:
>
> - fall through by default - a source of bugs and confusion
> - case clauses that act like blocks but without their own scope, such
> that variable declarations clash with other cases (hit me again today)

I'll quibble with the gratuitous hyperbole of "deep flaws", but let's 
stipulate these were design errors (though, if you allow fallthrough at 
all, it's not obvious that the alternatives on the second one are better.)

I'll just also point out that, while I am as offended by mistakes as you 
are, my experience has taught me that the desire to "fix mistakes of the 
past" is one of the most dangerous temptations a language designer can 
indulge in.  Our offense at the "mistake" can blind us to the cost of 
the "solution".

Clearly, in 1995, we could have -- and, with perfect knowledge, should 
have -- made different choices.  We could have made `break` the default 
when you hit another case label, and had an explicit `fallthrough` 
statement.  The same set of programs would be expressible in the 
language, but this default would be biased towards the overwhelmingly 
common case, and would be less error-prone.  Doh.

Could we fix this now?  Well, we could start now.  We could introduce a 
`fallthrough` statement, and maybe warn when you fell through without it 
(we actually have a lint warning for this now.) Over time, we could make 
falling through without saying `fallthrough` an error.  Over a much 
greater period of time, we could _maybe_ consider inferring break when 
you hit a case statement, if we were confident that every program that 
fell through without saying `fallthrough` had been recompiled and fixed, 
and all programmers suitably reeducated.

That would take a long time.  And there would be multiple dimensions of 
cost.  There's obviously the cost to design, bikeshed, implement, 
bikeshed, and reimplement the feature.  There's the cost that we are 
introducing a feature that doesn't, initially, do anything other than 
document what's going on (you already get a lint warning today on 
fallthough.)  There's the cost to people learning the language, who will 
wonder we have a statement that essentially doesn't do anything.  
There's the cost to people who get warnings (and eventually errors) on 
code that has been correct and working for years.  There's the risk 
that, no matter how long we wait, we'll never be able to confidently 
make the switch to break-by-default. And until we get to this 
maybe-never-get-there end, the benefit is basically zero, except that it 
makes us satisfied we're doing something to fix a mistake of the past.  
Danger, Will Robinson.

Overall, while I wouldn't object on philosophical grounds to a statement 
like `fallthrough`, it also doesn't move us that far forward.  It's 
pushing on the short end of the lever.

So, OK, our esteemed forebearers got the default wrong here (and in 
other places).  Oh well.  Java's still pretty good.

> (The only use case for fall through I ever see presented is low level
> network code, which is very much an edge case. Is there some use case
> in pattern matching for fall through that I am unaware of?)

No question, it will be even more rare to have nontrivial (i.e., 
statements between the case labels) fallthrough with patterns than with 
primitives -- if nothing else, the intersection constraints on binding 
variables will catch more accidental fallthroughs at compile time.  
(Where I suspect you're going here is to make "falling into a pattern" 
an error, or at least a warning.  (There's nothing wrong with this line 
of thinking, though this seems more in the category of "icing on the 
cake", so I'm inclined to put in in the bucket of things to think about 
when all the actually hard problems are solved.)  But just as we'd like 
to avoid making an artificial distinction between "pattern switch" and 
"constant switch", there's also no real distinction between constant 
label and pattern; a constant label _is_ a pattern.  You could play a 
lot of games to divide patterns into categories, and maybe there's an 
acceptable tradeoff of where to draw this line, but this is still 
introducing complexity, and it still hasn't freed you of the really 
annoying part -- the need to say `break`.  So again -- short lever.)

> 2) Normally when adding a new feature the desire is to add the feature
> in a way that will makes sense viewed 10 years hence. However

I am well aware of  this stewardship principle  :)

What I think you may be missing is that we _are_ focusing on what will 
make sense 10 years hence -- by avoiding having two language constructs 
that are similar, but subtly different.  (Yes, there are going to be 
subtle differences no matter what; our goal is to put them where they 
hurt the least.)  Having one switch construct that has one set of 
semantics across statements and expressions, across patterns and 
constants, leads to a simpler and more consistent language.  (What it 
doesn't lead to is fixing mistakes of the past.)

It's surely tempting to say that `switch` is irredeemable, and we should 
"just" design a new statement (snitch? swatch?) that subsumes constant 
and pattern statement and expression switch with the "right" rules.  And 
while there's merit to that line of argument, the reality is that 
`switch` isn't going away -- there's too much code.  So Java developers 
would have to learn both, and remember their subtle differences.  Is 
that the language we want in 10 years?

Maybe you think `switch` will just go away, or even that we'll be able 
to deprecate it.  I don't believe this.  Nor do I think that would be a 
very effective place to spend our "break people's programs for their own 
good" budget (if this budget is even nonzero.)

> the
> current explanation for expression switch is couched in terms of 'its
> just the same old statement switch with a few new extras'. This gives
> the appearance of being a design for existing developers migrating,
> rather than a strong design for the longer term.

I think that's a cynical way to characterize it.  We're valuing building 
on what we have rather that forcing different and new stuff on people 
(likely for limited benefit.)

The last bit -- for limited benefit -- is pretty important.  If 
fallthrough-by-default were as bad as pervasive buffer overruns or 
dangling pointers, you can bet we'd be in a hurry to fix it.  But 
really, fallthrough is just a peeve.  Sure, it's irritating.  Saying 
`break` all the time is annoying (though with expression switches, we 
will have to far less often.)  But at heart, it's a minor annoyance, and 
it's one we've already gotten used to.  Doing major surgery on the 
language merely for the sake of fixing a minor peeve would be a bad 
trade.  We've done pretty well so far by the "new ways to use old 
constructs" approach; I think its going to work pretty well here too.

I'll admit that at the beginning of this exercise, I too thought it was 
near-impossible that we'd be able to introduce patterns and expressions 
into switches without breaking it, and we might be forced into a 
separate construct.  But, bit by bit, over hundreds of hours at the 
whiteboard, we chipped away at the accidental complexity, and (even to 
our own surprise) arrived at the place where the new concepts fit pretty 
well.

Might we still find a show-stopper that would drive us somewhere else?  
Sure, but I haven't seen it yet.

> 3) Expression switch is unlike any other kind of element in the
> language, and this is causing rough edges. It is not especially like a
> "normal" expression, nor like a "normal" statement or method.

Yes.  It's different (sort of) in a number of ways:
  - it starts with a keyword, has braces (but so can lambdas)
  - it might be much larger than typical expressions (but so can lambdas)
  - it can contain statements (but so can lambdas)
  - it has control flow statements (but so can lambdas)
  - it can interfere with nonlocal control flow out of enclosed 
statements (but so can lambdas)

These are certainly reasons to be wary.  Did we consider making 
"expression switches" look more like something else, like conditional 
expressions?  Of course we did.  But doing so has drawbacks too.  Among 
many other risks of this approach, there's a risk that slicing on this 
dimension drives us towards four constructs: 
{pattern,constant}x{expression,statement}.

The persistent "but so can lambdas" qualifiers are informative. Just as 
there are good ways and bad ways to use lambdas, there are going to be 
good and bad ways to use expression switches.  1000-line lambdas used as 
operands of conditional expressions, while legal, are probably a bad 
idea.  There will be similar "bad idea" style guidance for switch 
expressions.

What I take away from your (3) is that having switch be both an 
expression and a statement is not without risk.  And that's right; it's 
not.  Still, overall IMO the benefit of "one construct" still seems to 
be the winning move.

> // lots of code
>    case FOO:
>      if (bar == null) {
>        return "str";  // is this allowed or not?
>      }
>      // lots more code
>
> This is only allowed in a statement switch, yet because switches can
> be large, the context as to whether it is a statement or expression
> switch cannot be seen, ie. the context that determines code validity
> is too remote.

Sure, if you follow bad style, you're going to get hard-to-reason-about 
code.  If you've got an expression switch that's hundreds of lines long, 
probably you shouldn't be using it as a method parameter; you should 
factor it into a method.  (Just like lambdas.)  In any case, the user 
will get feedback here from the IDE or compiler.

On the subject of record/continue/no-expression-break, these will all be 
illegal at (the top level of) an expression switch, and a nested 
switch/for/while will not be able to labeled-break "through" the 
expression switch barrier (just as with lambda expressions.) Nor will an 
enclosed switch/for/while be able to break-value out of the enclosing 
expression switch.  This outlaws some potentially legitimate but tricky 
code, in favor of making the "exit points" from an expression switch far 
more obvious.

> However, I would ask for consideration to allow "break :label" and
> "continue :label" (a colon before the label) to be valid syntax, so
> that over time developers could transition to a clearer way to express
> labelled breaks distinct from expression breaks.

That seems like a reasonable thing to consider (though the Rubyist's in 
the audience will be sad that we foreclosed on symbols.)

> 4) The automatic default clause for enums seems good and desirable,
> but it will be odd not having it in statement switches.

I would call it sad, but not odd, because it's essentially a forced 
move.  This code:

     switch (e) {
         case A: foo();
     }

means the same thing as:

     if (e == A)
         foo();

And, there's nothing wrong with incomplete statement switches, just as 
there's nothing wrong with unbalanced ifs.  But, again, I think you're 
falling into the trap of thinking about it as two different constructs; 
it's not -- it's just a straightforward application of existing flow 
analysis to a new kind of expression.  We apply exhaustiveness 
constraints to existing switch statements too, when a blank local is 
involved:

     String x;
     switch (y) {
         case 0: x = "ZERO"; break;
         case 1: x = "ONE"; break;
         // no default
     }
     g(x);  <-- x is DU point of use, error

Here, the ordinary DA/DU flow analysis says that x is DU at the point of 
use, and yields an error.  If a switch is an expression, it has to be 
total, and flow analysis verifies that for us.

Adding in the implicit throwing default for enums (and later, sealed 
types) is a nice thing to do, but it is just that.  We don't have to do 
it; we could make people keep adding a throwing default, and it wouldn't 
change anything, other than be less fun to use.  That we can do this for 
expression switches on enums and sealed types is great (boolean too, 
maybe even byte); that we can't do it for non-sealed types is completely 
unsurprising; that we can't do it for statement switches over sealed 
types without some sort of opt-in request for exhaustiveness ("sealed 
switch") is sad, but the price of the (entirely reasonable!) choices we 
made to allow partial switches and unbalanced ifs.    (There will be 
similar "best effort" cases when we get to exhaustiveness analysis with 
more complex patterns, especially if we allow guards; there will be 
"obviously exhaustive" switches that the compiler can't prove 
exhaustiveness on, and you'll have to "close" the switch with a 
catch-all pattern.)

We could restore balance by not trying to do it at all, but that seems 
throwing the baby out with the bathwater.

> Finally, as far as I can tell, the following is legal in the current
> proposal. Is this true?:
>
> var action = switch (trafficLight) {
>   case RED: System.out.println("Found red"); // fallthru
>   case YELLOW -> "Stop";
>   case GREEN:
>     System.out.println("Found green");
>     break "Green";
> }

Yes.
> (I find this mixture of expression and statement styles complex and ugly)

I agree it is complex and ugly.  (It would be even uglier, and more 
error-prone, and far far more common, if you could also use the -> 
shorthand form for single statements in a statement switch.)

But, I am not inclined to try and legislate against it.  We have style 
guides, IDE refactorings, and peer pressure for that, and I doubt people 
will actually want to do it very often.  Nor do I think that the 
existence of such idioms is evidence of a flaw.  Languages provide you 
with general tools for saying what you mean; there are often many 
different ways to say the same thing, some better than others.

There is one case in which this "mixed style" idiom will be common and 
totally legitimate: expression switches with recovery actions on default 
clauses:

     var action = switch (light) {
         case RED -> STOP_ACTION;
         case YELLOW, GREEN -> FLOOR_IT_ACTION;
         default:
             log("WTF: " + light);
             throw new WtfException("Unexpected color: " + light);
     }

Here, the "normal" paths will be handled with single expressions and no 
funny business, but the exceptional path will require some statements, 
either to document and handle an error condition, or to construct an 
unusual return value.

To get to your unusual example, you have to combine several things: 
mixing `:` and `->`, falling through, and further, falling through into 
a clause that completes normally.  This will be a rare combination indeed.

In the icing department, once the arguments over the actually 
significant design points are through, we can consider considering 
safety rails like treating falling through into a -> case to be a 
warning (or worse).  But, if we're still arguing that we've taken the 
wrong path, there's no point in such edge-polishing.




More information about the amber-dev mailing list