Call for bikeshed -- break replacement in expression switch

John Rose john.r.rose at oracle.com
Thu May 16 19:53:27 UTC 2019


FTR I'm OK with "yield".  (I yield the floor?)

(And I'm OK with "pass", but we'll probably
pass on that option?)

The rule, I take it, is that `yield x;` would
deliver a value to the innermost enclosing
`->` operator.  If it could be that simple,
that would be a win; we could teach our
eyes and IDEs to make that match-up.

As I think you've said, such a rule that keys
on `->` would allow us to apply yield
retroactively to lambdas, *and* to switches,
*and* to hypothetical expression-blocks
in the future (if they have a `->` at their
head, the rule applies uniformly), *and*
to concise method bodies, as an alternative
(as with lambda) to return.

What about return-vs-yield?  Well, yield
is OK when there's a matching `->`.
And return is OK when you're in a
method body (and not also in a `->`).
So sometimes both rules apply; pick
a keyword; tastes may vary.  That's
not too different an experience from
equivalent break vs. return (when the
break falls out of the method body).

On May 16, 2019, at 8:24 AM, Brian Goetz <brian.goetz at oracle.com> wrote:

> 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

Let's be careful of how we apply this ordering.
A yield (like a lambda-return) can unwind any
number of control constructs, up to the innermost
yieldable expression.

Because yields don't take labels, they cannot
even express a multi-expression exit.  But
they *naturally* entail multi-block exit.

Searches involving loops, catches, and ifs
are common in Java and therefore essential
to support with yield:

L0: for (var q : qstuff) {
L1: f(q, ()->{
STARTER -> { //B0
     //break L0; continue L0; => BAD JUMP
B1: try {
     B2: for (var x : stuff) {
         B3: if (x.stopHere())  yield x;
     } catch (MyEx ex) { yield ex.getStuff(); }
     B4: if (lastChance)  yield DEFAULT_STUFF;
     throw new ComplainingEx();
} } } }

Here, any of the blocks Bi could be unwound
by a yield.  The yield only goes back to the
STARTER (which could be a lambda, switch
or futuristic thing). A yield cannot reach the
outer lambda at L1.

More over, the break L0 would be a bad jump,
since it cannot break out of the -> of the STARTER
expression.

Going back to your list of "unwind strength",
I think *breaks* are therefore more limited than
yields:

 - break/continue: can unwind multiple control constructs (for, while, switch), but stays within *both* the method and the innermost `->`
 - yield: can unwind multiple control constructs (for, while, switch), but stays within the innermost `->`
 - return: unwinds exactly one method frame (implicit after `->` method body)
 - throw: unwinds one or more methods
 - System.exit: unwinds the whole VM

One more side note:  Yield in a lambda can be
viewed as jumping to the very outside of the
lambda body, with a value, at which point
"return off the end" takes over.  So every yield
can be considered a frame-local operation
(perhaps followed by an implicit "return off
the end, but with a value").  The reason I'm
making this distinction is that it lets us say
that yield always stays *inside* a method
activation frame (even if the next step is
to return the yielded value).

This "yields" a uniform rule:  If a `->` is
immediately inside a block which defines
local variables, those variables are available
to code around the yield *for mutation*
as well as reading.  This is a different rule
than with lambda uplevels.  It allows code
which yields an expression to *also* yield
additional values by assigning to up-level
variables.  This too is a common pattern
in Java.  For example, a loop might return
both an array element and the index of that
element, to set up later searches starting
after that index.

int res2;
var res1 = STARTER -> {
   … res2 = 42; yield myRes1Value;
};
System.out.println("got em: "+asList(res1, res2));

So why can't lambdas side-effect out?
Simple, because they are -> blocks
invisibly and immediately nested inside
of method bodies.  There are no vars
declared which will survive the implicit
return operation, so there's nothing to
share (writably) with an enclosing block.
But you can say "x = 1; yield 2;" usefully
if the enclosing -> block is not also
a method body.

— John


More information about the amber-spec-experts mailing list