Call for bikeshed -- break replacement in expression switch

Brian Goetz brian.goetz at oracle.com
Thu May 16 15:24:36 UTC 2019


We’ve probably pretty much explored the options at this point;  time to converge around one of the choices...

> 
> De: "Brian Goetz" <brian.goetz at oracle.com>
> À: "amber-spec-experts" <amber-spec-experts at openjdk.java.net>
> Envoyé: Dimanche 12 Mai 2019 21:38:38
> Objet: Call for bikeshed -- break replacement in expression switch
> As mentioned in the preview mail, we have one more decision to make: the new spelling of “break value” in expression switches.  We have previously discussed “break-with value”, which everyone seems to like better than “break value”, but I think we can, and should, do better. 
> 
> (Despite the call-for-bikeshed, this is not to reopen every sub-decision — the 2x2 semantics, the use of ->, the name of the construct — this bikeshed only has room for one bike.)
> 
> There are two primary reasons why we prefer break-with to break.  We originally chose “break value" when we had a more limited palette of options to choose from (the keyword-resupply ship hadn’t yet docked.)  The overloading of break creates uncomfortable interactions.  There is the obvious ambiguity between “break value” and “break label”; there is also the slightly less obvious interaction where we cannot permit “break value” inside a loop or statement switch inside an expression switch.  While both of these can be “specified around”, they create distortions in the spec, which in turn creates complexity in the user model; these are a sign that we may be pushing something a bit too far.  Further, historically “break” has been a straight transfer of control; this muddies up what “break” means.  
> 
> Once we alit on the idea of break-* as a keyword, it seemed immediately more comfortable to make a new break-derived keyword; this allowed us to undo the distortions that “break value” introduced, and it immediately felt better.  But I think we can do better still.  Here’s what’s making me uncomfortable.  
> 
> We’ve actually been here before: lambda expressions were the first time we allowed an expression to contain statements, and while the streamlined case of “x -> e” didn’t require any control statements, and many lambdas could be expressed with this form, statement lambdas needed a way to say “stop executing the body of this lambda, and yield a value.”  We settled — somewhat uncomfortably — on “return value" for this.  
> 
> Fast-forward to today, when we’re introducing the second expression form that can contain statements, and we face the same question: how to indicate “I’m done, I’m completing normally, here’s my value.”  Lambdas provide no help here; we can’t use “return” here.  (Well, we could, but that would be terrible, so we’re not going to.) Which means we have to solve the problem again, but differently.  That’s already not so great.  
> 
> Digression: What’s so terrible about “return”, any why is it OK for lambdas but not OK for switches?  
> 
> While we could of course define “return” to mean whatever we want, But, in imperative languages with the concept of “methods” or “procedures”, including Java, return has always had a clear meaning: unwind the current call frame, and yield the designated value to the caller.  Lambda expressions are effectively method bodies (lambdas are literals for functional interfaces, which are single method interfaces), and so return (barely) fits.  But switch expressions are most definitely not methods, and are not associated with call frames. Asking users to look at the enclosing context when they see a “return” in the middle of a method, to know whether it returns from the method or merely transfers control within the method, is a lot to ask.  (Yes, I know lambdas ask this as well; this is why this was an uncomfortable choice, and having made this hole, I’m not anxious to expand it dramatically.  If anything I’d prefer to close it, but that’s another bikeshed.). 
> 
> (end digression)
> 
> 
> We could surely take “break-with” and move on; it feels sufficiently “switchy”.  But let’s look ahead a little bit.  We’ve now confronted the same problem twice: an expression form that, in a minority use case, needed a way to express “stop computing this expression, because I’m done, and here’s its value.”   (And, unfortunately, we have two different syntactic ways to express the same basic concept.)   Let’s call these “structured expressions.”  
> 
> We have two structured expression forms, and of the three numbers in computer science, “two” is not one of them.  Which suggests we are going to face this problem again some day — whether it be “block expressions”, or “if expressions”, or “let expressions”, or “try expressions”, or whatever.  (NB: this call-for-bikeshed most definitely does not extend to “why not just do generalized block expressions”, so please don’t go there.  That said, you could treat this discussion as “if Java had block expressions, what might they look like?”  But we’re focusing on the content of the block, not how the block is framed.) 
> 
> Let’s say for sake of argument that we might someway want to extend ternary expressions to support the same kind of “restricted block expressions” as expression switches.  (This is just an example for purposes of illustration, let’s not get derailed on “but you should use an ‘if’ statement for that"). 
> 
>     String s = (foo != null) 
>         ? s
>         : {
>              println(“null again at line” + __LINE__);
>              break-with “null”;
>           };
> 
> Such an expression needs a way to say “I’m done, here’s my value”, just as lambda and switch did before it. Clearly “return” is not the right thing here any more than it is for switches.  And I don’t think “break-with” is all that great here either!  It’s not terrible, but outside of a loop or switch, it starts to feel kind of forced.  And it would be terrible to solve this problem twice with one-time solutions, and have no general story, and then have to come up with YET ANOTHER way of expressing the same basic concept.  So regardless of what we expect for future expression forms, let’s examine what our options are that are not tied to call frames (return) or direct transfer of control (switches and loops.). 
> 
> Looking at what other languages have done here, there are a few broad directions: 
> 
>  - A statement like “break-with v”, indicating that the enclosing structured expression is completing normally with the provided value.  
>  - An operator that serves the same purpose, such as “-> e”.
>  - Assigning to some magic variable (this is how Pascal indicates the return value of a function).  
>  - Treating the last expression in the block as the result.  
> 
> I think we can dispatch all but the first relatively easily:
> 
>  - We don’t use operators for “return”, we use a keyword; this would be both a gratuitous departure, as well as too easy to miss.
>  - Switch expressions don’t have names, and even if we assigned to “switch”, it wouldn’t be obvious that we were actually terminating execution of the block. 
>  - Everywhere else in the language (such as method bodies), you are free to yield up a value from the middle of the block, perhaps from within a control construct like a loop; restricting the RHS of case blocks to put their result last would be a significant new restriction, and would limit the ability to refactor to/from methods. And further, the convention of putting the result last, while a fine one for a language that is “expressions all the way down”, would likely be too subtle a cue in Java.  
> 
> So, we want a keyword (or contextual keyword.).  In some hallway brainstorming, candidates that emerged include yield, produce, offer, offer-up, result, value-break, yield-value, provide, resulting-in, break-with, resulting, yielding, put, give, giving, ...
> 
> (Also to keep in mind: remember we’re dealing with a minority case; most of the time, there’ll just be an expression on the RHS.)
> 
> TL;DR: I think we might come to regret break-* just as we did with return — because it won’t scale to future demands we place on it, and having *three* ways to say basically the same thing in three different contexts would be embarrassing.  I would like to see if we can do better.
> 
> 
> Of the options listed here, I have a favorite: yield.  (This is one of the terms we’ve actually be using all along when describing this feature in english.)  
> 
> There is one obvious objection to “yield”, which I’d like to preemptively address: that in some languages (though not in Java, except for the infrequently-used Thread.yield()), it is associated with concurrency primitives, such as generators.  (This was the objection raised when yield was proposed in the context of lambdas.). But, these association are not grounded in existing Java constructs (and, the progress of Loom suggests that constructs like async/await are not coming to Java, and even if we wanted language support for generators, there are ample other ways to say it.)
> 
> Dictionary.com <http://dictionary.com/> lists the following meanings for yield: 
> 
> verb (used with object)
>  - to give forth or produce by a natural process or in return for cultivation:
>  - to produce or furnish (payment, profit, or interest):
>  - to give up, as to superior power or authority:
>  - to give up or over; relinquish or resign:
>  - to give as due or required:
>  - to cause; give rise to:
> 
> verb (used without object)
>  - to give a return, as for labor expended; produce; bear.
>  - to surrender or submit, as to superior power:
>  - to give way to influence, entreaty, argument, or the like:
>  - to give place or precedence (usually followed by to):
>  - to give way to force, pressure, etc., so as to move, bend, collapse, or the like:
> 
> These are mostly consistent with the use of “yield” as proposed here.  
> 
> One more thing to bear in mind: there is an ordering to abrupt completion mechanisms, as to how far away they can transfer control:
> 
>  - yield: can unwind only the innermost yieldable expression
>  - break/continue: can unwind multiple control constructs (for, while, switch), but stays within the method
>  - return: unwinds exactly one method
>  - throw: unwinds one or more methods
>  - System.exit: unwinds the whole VM
> 
> 
> Bikeshed is open (but remember the bounds of this bikeshed are limited; we’re talking purely about the syntax of a “stop executing this block and yield a value to the enclosing context” — and time is ticking.)
> 
> 
> 
> 
> 



More information about the amber-spec-observers mailing list