Expression switch feedback

Cay Horstmann cay.horstmann at gmail.com
Sun Apr 8 15:23:15 UTC 2018


Since I will be one of the people who will have to explain this feature,
let me just say a few things.

1. It will be confusing that the expression alternative to if/else is ? :
and the expression alternative to switch is, erm, switch.
2. The fallthrough behavior in switch is confusing enough for a statement.
For an expression, it is very, very counterintuitive.
3. break "Green"???

As a book author, I am never fond of having to explain odd syntax and
fiddly edge cases that are there for historical reasons. In fact, for over
20 years, I suggested to my readers that it was ok to stay away from the
switch statement. I would hate to make a similar recommendation for an
expression switch.

I re-read all of Brian's arguments about subtle differences between
statement and expression switch, and I find them hard to swallow. This is a
self-inflicted injury, solely based on calling both of them switch.

Now you may argue "but we want to capture the useful intuition that people
already have from the switch statement". To which I say "there is precious
little useful intuition to be captured--just read through the previous
messages". I am pretty sure that many blue-collar programmers have a
general understanding of switch as a multi-selector and a well-founded fear
of the break statement. That's not a solid foundation to build on.

My strong recommendation would be to leave statement switch alone. It's
reprehensible in so many ways, but it is well understood in its current
form. Make a match expression, or whatever you want to call it. Make it
sane and clear. You've gone through that design space. I can write with
conviction "match (or whatever you'll call it) is to switch what ? : is to
if", and everyone is going to get it right away. And nobody will say
"that's weird--why didn't they make it confusing like switch"?

Just saying,

Cay


2018-04-08 8:21 GMT-07:00 Cay Horstmann <cay.horstmann at gmail.com>:

> Since I will be one of the people who will have to explain this feature,
> let me just say a few things.
>
> 1. It will be confusing that the expression alternative to if/else is ? :
> and the expression alternative to switch is, erm, switch.
> 2. The fallthrough behavior in switch is confusing enough for a statement.
> For an expression, it is very, very counterintuitive.
> 3. break "Green"???
>
> As a book author, I am never fond of having to explain odd syntax and
> fiddly edge cases that are there for historical reasons. In fact, for over
> 20 years, I suggested to my readers that it was ok to stay away from the
> switch statement. I would hate to make a similar recommendation for an
> expression switch.
>
> I re-read all of Brian's arguments about subtle differences between
> statement and expression switch, and I find them hard to swallow. This is a
> self-inflicted injury, solely based on calling both of them switch.
>
> Now you may argue "but we want to capture the useful intuition that people
> already have from the switch statement". To which I say "there is precious
> little useful intuition to be captured--just read through the previous
> messages". I am pretty sure that many blue-collar programmers have a
> general understanding of switch as a multi-selector and a well-founded fear
> of the break statement. That's not a solid foundation to build on.
>
> My strong recommendation would be to leave statement switch alone. It's
> reprehensible in so many ways, but it is well understood in its current
> form. Make a match expression, or whatever you want to call it. Make it
> sane and clear. You've gone through that design space. I can write with
> conviction "match (or whatever you'll call it) is to switch what ? : is to
> if", and everyone is going to get it right away. And nobody will say
> "that's weird--why didn't they make it confusing like switch"?
>
> Just saying,
>
> Cay
>
>
>
>
> 2018-04-08 7:49 GMT-07:00 Brian Goetz <brian.goetz at oracle.com>:
>
>>
>> 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