Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

loop-break-value (issue #961) #1624

Merged
merged 14 commits into from Oct 22, 2016

Conversation

@dhardy
Copy link
Contributor

commented May 20, 2016

This is the culmination of recent discussion in issue #961, minus discussion about extensions to for/while/while let (for which there is no clear solution).

@llogiq

This comment has been minimized.

Copy link
Contributor

commented May 20, 2016

Rendered

[motivation]: #motivation

This pattern is currently hard to implement without resorting to a function or
closure wrapping the loop:

This comment has been minimized.

Copy link
@sfackler

sfackler May 20, 2016

Member

It's not that hard:

fn f() {
    let outcome;
    loop {
        // get and process some input, e.g. from the user or from a list of
        // files
        let result = get_result();

        if successful() {
            outcome = result;
            break;
        }
        // otherwise keep trying
    };

    use_the_result(outcome);
}

This comment has been minimized.

Copy link
@dhardy

dhardy May 20, 2016

Author Contributor

Yes, you're right. I was able to reduce my problem to this pattern:

let computation = {
    let mut result = None;
    loop {
        if let Some(r) = self.do_something() {
            result = Some(s);
            break;
        }
    }
    result.unwrap().do_computation()
};
self.use(computation);

Lifetimes make some of these problems horrible to think about.

This comment has been minimized.

Copy link
@dhardy

dhardy May 21, 2016

Author Contributor

Damn. The problem with being British is you expect people to spot the sarcasm.

  1. Having to mutable a variable just to return a value is inelegant
  2. Having to wrap with Option then unwrap just because there isn't an appropriate default value is clumsy
  3. Having to do this while considering the lifetimes of generated references is confusing
  4. It's a lot of extra code. Compare the above to:
let computation = loop {
        if let Some(r) = self.do_something() {
            break r;
        }
    }.do_computation();
self.use(computation);

(noting that due to reference lifetimes it may be possible to give the value fed to do_computation() a name in the outer scope).

Is that sufficient motivation?

The new lifetime rules under discussion may well simplify this problem, both with-and-without break-with-value, but I still think break-with-value is a neat pattern (or alternately, a generic break-from-block-with-value, but that opens another can of worms: the value if not broken out of).

This comment has been minimized.

Copy link
@birkenfeld

birkenfeld May 21, 2016

Well, @sfackler 's example works fine without a mutable variable or an Option. Am I missing something?

This comment has been minimized.

Copy link
@dhardy

dhardy May 21, 2016

Author Contributor

Maybe, and maybe it's a good thing. Sounds like it's time I un-learned to never leave variables uninitialised (thanks to Rust's great flow & lifetime analysis).

Points 3 and 4 still hold (the example above can be four words shorter), but this weakens the argument.

This comment has been minimized.

Copy link
@birkenfeld

birkenfeld May 21, 2016

Point 3 - please give an example where these confusing-to-consider lifetimes are involved.
Point 4 - "a lot of extra code" when talking about essentially one extra assignment?

This comment has been minimized.

Copy link
@JelteF

JelteF May 21, 2016

The current pattern is not necessarily hard, but the main problem I think is that the new pattern expresses much better what is going on.
It is immediately clear that the loop is meant to set the value, while with the code of @sfackler this is not te case. Especially when considering loops with more content or with multiple exit points.


* if a loop is "broken" via `break;` or `break 'label;`, the loop's result type must be `()`
* if a loop is "broken" via `break EXPR;` or `break 'label EXPR;`, `EXPR` must evaluate to type `T`
* as a special case, if a loop is "broken" via `break EXPR;` or `break 'label EXPR;` where `EXPR` evaluates to type `!` (does not return), this does not place a constraint on the type of the loop

This comment has been minimized.

Copy link
@birkenfeld

birkenfeld May 20, 2016

I don't see the need for this special case: there's no reason to write break EXPR; instead of EXPR; when the expr is diverging.

This comment has been minimized.

Copy link
@dhardy

dhardy May 20, 2016

Author Contributor

No, and I suppose this is why it's not legal to type let x: ! = ...;.

This comment has been minimized.

Copy link
@birkenfeld

birkenfeld May 20, 2016

Well, that's because ! is not a real type (yet - there is an RFC open for that) and is only allowed in function return type notation.

This comment has been minimized.

Copy link
@nagisa

nagisa Oct 4, 2016

Contributor

Now ! is a legal type everywhere, but this special case is still useful as a note, even though it follows just fine from rules applicable to ! (i.e. ! coerces to any other type).

(EDIT: Weird, this was a reply to an existing thread on the same line; explains why it has no the reply field though…)

### Result type of loop

Currently the result-type of a 'loop' without 'break' is `!` (never returns),
which may be coerced to any type), and the result type of a 'loop' with 'break'

This comment has been minimized.

Copy link
@birkenfeld

birkenfeld May 20, 2016

consistency nit: "result-type" vs "result type"

@durka

This comment has been minimized.

Copy link
Contributor

commented May 21, 2016

Btw, I have a scary macro that mostly implements this as well as labeled blocks, in case you want to play around.

@notriddle

This comment has been minimized.

Copy link
Contributor

commented May 24, 2016

Considering the fact that other blatantly stupid constructs like println!("{:?}", panic!()) type-check, do we really want to require break's expression to converge? AFAIK, this would be the first place in the entire language that explicitly bans diverging expressions.

@upsuper

This comment has been minimized.

Copy link

commented May 24, 2016

for and while may need something like Python's for-else structure. That could be another RFC I suppose.

@dhardy

This comment has been minimized.

Copy link
Contributor Author

commented May 24, 2016

@notriddle good point; I have no idea.

@upsuper: already discussed a lot; explicitly not part of the RFC

@golddranks

This comment has been minimized.

Copy link

commented May 24, 2016

I think that the requirement for EXPR in break EXPR; to converge, as stated in the RFC text, is unnecessary. Like @notriddle said, the expressions in Rust work even without such requirements, so changing that would be a complication.

```rust
assert_eq!(loop { break; }, ());
assert_eq!(loop { break 5; }, 5);
let x = 'a loop {

This comment has been minimized.

Copy link
@solson

solson May 24, 2016

Member

syntactic nit: this should be 'a: loop { and 'b: loop { on the line below.

@chris-morgan

This comment has been minimized.

Copy link
Member

commented May 25, 2016

break value should be behaving exactly like return value, except producing a value for a loop rather than a function. return break break return break up (please, continue) should therefore compile (presuming a suitable function up and variable please), just as return return return return return works fine now.

I look forward to this compiling:

fn main() {
}

struct X;
type B = Result<(X), (X)>;  // That signature looks scary.
const please: () = ();
macro_rules! escape { ($x:expr, $y:expr) => (()); }
fn out(_: (), _: ()) { }
const now: B = Ok(X);

fn attempt_escape() -> B {
    'prison: loop {
        if break out (please, continue) {
            escape! (oh, yeah?)
        } else {
            return now :(
            // You got caught. Maybe next time you should try wearing cool sunnies like this:
            B)
        }
    }
}

@aturon aturon self-assigned this Jun 2, 2016

@wycats

This comment has been minimized.

Copy link
Contributor

commented Jun 18, 2016

I like this improvement a lot.

Ruby is also an expression-oriented language and allows break EXPR. Even though I don't use the feature all that often in Ruby, it's pretty easy to remember (it has a good analogue with return) and makes code terser without a loss in clarity when it's useful.

I especially like the fact that this is a nice improvement to an existing expression form with minimal and intuitive new syntax.

Like the original issue thread, I have some concerns with other extensions, and am glad this RFC split out just break EXPR so it could be evaluated on its own.

@nikomatsakis

This comment has been minimized.

Copy link
Contributor

commented Jun 29, 2016

I'm basically 👍 on this. Limiting to loop makes sense to me, at least initially.

@dhardy

This comment has been minimized.

Copy link
Contributor Author

commented Jun 29, 2016

Nice. I'm still watching this but from what I gather no changes are needed to the RFC?

@dhardy dhardy closed this Jun 29, 2016

@dhardy

This comment has been minimized.

Copy link
Contributor Author

commented Oct 3, 2016

@vitiral interesting point. But the RFC states that break (); is equivalent to break;, and as I understand it the return value of a statement ending ; is (), so if there is no break in the else block, the Python-esque for ... else ... with no break or return value would be completely legal (I think).

Besides, sometimes side-effects are much more important than return values. TBH I don't understand the Python ambiguity problem very well since it always seemed clear to me (yes, in Python).

@vitiral

This comment has been minimized.

Copy link

commented Oct 3, 2016

the return value of any statement ending in ; is (), so you could write:

for name in suggest_names() {
    if like(name) {
        print!("{}", name);
        break;
    }
} else {
    let last_resort = random_name();
    print!("{}", last_resort);
}

So you don't need a break in the else clause (just like you don't need one in if ... else even though that can return a value)

for ... else without break in the for loop would not be legal in rust, since the for block would not have a return value. This is good, as for ... else without break makes no sense at all -- else would always get executed (assuming you didn't return or panic).

@Amanieu

This comment has been minimized.

Copy link
Contributor

commented Oct 3, 2016

Would it be better if finally (obviously this is open to bikeshedding) was used instead of break? This makes it clearer that this block is only executed if no break occurred in the loop.

let x = for name in suggest_names() {
    if like(name) {
        break name;
    }
} finally {
    random_name()
};
@vitiral

This comment has been minimized.

Copy link

commented Oct 3, 2016

@Amanieu do you mean use finally instead of else? No, I think that would be even worse. For one thing, in python finally means "execute me no matter what".

@Diggsey

This comment has been minimized.

Copy link
Contributor

commented Oct 3, 2016

I'd like to know from the people who object to just using for {} else {} etc on the basis that it's confusing, whether using another keyword instead of else would solve the problem for them, or whether they'd still find it confusing?

If not, there's no point bike-shedding further on alternative names. The choices involving Option or some variation thereof seem to be flawed, so it's a simple choice between else or nothing.

@JelteF

This comment has been minimized.

Copy link

commented Oct 3, 2016

I'm still very pro my suggestion of using !break. I think it really
simply shows the meaning of the second block. Without even reading the loop
body (and seeing that values are being returned with break).

On 3 Oct 2016 11:09 pm, "Diggory Blake" notifications@github.com wrote:

I'd like to know from the people who object to just using for {} else {}
etc on the basis that it's confusing, whether using another keyword instead
of else would solve the problem for them, or whether they'd still find it
confusing?

If not, there's no point bike-shedding further on alternative names. The
choices involving Option or some variation thereof seem to be flawed, so
it's a simple choice between else or nothing.


You are receiving this because you were mentioned.
Reply to this email directly, view it on GitHub
#1624 (comment), or mute
the thread
https://github.com/notifications/unsubscribe-auth/ABG8Ju5D5W2cQu0XkSMTYNnKKVJxtV-Cks5qwW8dgaJpZM4Ijc2V
.

@glaebhoerl

This comment has been minimized.

Copy link
Contributor

commented Oct 3, 2016

Something like the last 10 comments (I didn't count) are reinventing or rehashing ideas, and then the objections to them, almost verbatim from the other thread. Nicely illustrating, if anyone needed it, why that feature was deliberately left out of this RFC.

@aturon

This comment has been minimized.

Copy link
Member

commented Oct 4, 2016

@rfcbot resolved Cost/benefit and tail calls

I've been persuaded that, on the whole, this RFC moves the language toward greater uniformity, and that in particular proper tall calls are not necessarily a better choice. Let's do it!

@rfcbot

This comment has been minimized.

Copy link

commented Oct 4, 2016

All relevant subteam members have reviewed. No concerns remain.

@aturon

This comment has been minimized.

Copy link
Member

commented Oct 4, 2016

🔔 This RFC is entering its final comment period, with disposition to merge. 🔔

@nagisa
Copy link
Contributor

left a comment

I have some concerns and general style nits.


### Result type of loop

Currently the result type of a 'loop' without 'break' is `!` (never returns),

This comment has been minimized.

Copy link
@nagisa

nagisa Oct 4, 2016

Contributor

s/'/`/

This comment has been minimized.

Copy link
@dhardy

dhardy Oct 5, 2016

Author Contributor

The RFC is written in English, not code. shrug. 'loop' isn't a code snippet in this context, it's a quoted keyword.

### Result type of loop

Currently the result type of a 'loop' without 'break' is `!` (never returns),
which may be coerced to any type), and the result type of a 'loop' with 'break'

This comment has been minimized.

Copy link
@nagisa

nagisa Oct 4, 2016

Contributor

s/'/`/

I will not nit on any more of the formatting, but the issues with it probably should be fixed across the document. Namely, plain apostrophe (') does not begin/end an inline code snippet.

* if a loop is "broken" via `break;` or `break 'label;`, the loop's result type must be `()`
* if a loop is "broken" via `break EXPR;` or `break 'label EXPR;`, `EXPR` must evaluate to type `T`
* as a special case, if a loop is "broken" via `break EXPR;` or `break 'label EXPR;` where `EXPR` evaluates to type `!` (does not return), this does not place a constraint on the type of the loop
* if external constaint on the loop's result type exist (e.g. `let x: S = loop { ... };`), then `T` must be coercible to this type

This comment has been minimized.

Copy link
@nagisa

nagisa Oct 4, 2016

Contributor

This list still has issues with handling of coercion. For example I imagine something like this ought to be valid:

loop {
     // ...
     break (expr: SomeT);
     // ...
     break (expr: OtherT);
}: RT

where SomeT and OtherT both coerce to RT. If this discrepancy was fixed, the list could avoid having to contain the last two notes/requirements. I also dislike those two extra requirements-exceptions-rules, because they have potential to diverge from type rules elsewhere in the language.

(Why this matters? Substitute SomeT = &[u8; 42], OtherT = &[u8; 32], RT = &[u8].)

This comment has been minimized.

Copy link
@Ericson2314

Ericson2314 Oct 5, 2016

Contributor

I think the typing and coersions rules here can be exactly like those for match arms or if-else.

This comment has been minimized.

Copy link
@dhardy

dhardy Oct 5, 2016

Author Contributor

Please write a diff against the RFC; I was not 100% sure how to handle coercions (same with the ! special case).

@JelteF

This comment has been minimized.

Copy link

commented Oct 6, 2016

I just created a new issue to discuss other implementation details for returing from for and while than the the name of keywords. This issue can be found here for other people that are interested: #1767

@burdges

This comment has been minimized.

Copy link

commented Oct 7, 2016

Just noticed this slightly confusion loop that is not a loop :

let x = 'a loop { 
    ...
    if foo { break `a bar; }
    ...
    break `a baz;
};

It's obvious doing this with a closure would be more readable, but that's frequently true for loops too, if one had tail call optimization, so..

Is it desirable to support a less confusing form of this?

let x = 'a { 
    ...
    if foo { break `a bar; }
    ...
    baz
};

If so, does continue work in such a block? I presume no.

@glaebhoerl

This comment has been minimized.

Copy link
Contributor

commented Oct 7, 2016

@burdges That's the orthogonal (but synergistic) "break from any block" feature, I'm surprised I can't find any existing issue or RFC for it. cc @Ericson2314?

@Ericson2314

This comment has been minimized.

Copy link
Contributor

commented Oct 7, 2016

@glaebhoerl I don't think anybody wrote an RFC but we've both brought it up many times (e.g. ? And catch). Maybe it will finally stick :).

@vitiral

This comment has been minimized.

Copy link

commented Oct 7, 2016

@burdges this would not be backwards compatible. If I had code like:

loop {
    // do something
    {
        let foo = Bar();
        if x == foo {
            break;
        }
    }
}

currently this would break out of the loop block, but if we implemented your code it would break out of the inner block (and the loop would continue). I also don't like making break into a kind of goto (even though it often fulfills that role). If we want something like a generalized goto we should reserve a word for it and implement it separately rather than overload break

1. `break;`
2. `break 'label;`
3. `break EXPR;`
4. `break 'label EXPR;`

This comment has been minimized.

Copy link
@nagisa

nagisa Oct 7, 2016

Contributor

Is the ; after break EXPR compulsory? Currently existing forms of break do not require a semicolon after if they are in expression position.

Could we get an example of what would the grammar look like with syntax changes? See this and this.

This comment has been minimized.

Copy link
@vitiral

vitiral Oct 7, 2016

I would say that the ; is not necessary. It should map how return works, and the following compiles:

fn test() -> bool {
    if true {
        return true
    } else {
        return false
    }
}

fn test2() -> bool {
    return false
}

This comment has been minimized.

Copy link
@Ericson2314

Ericson2314 Oct 7, 2016

Contributor

Yes just like today

---------------
(break 'a e): !
@burdges

This comment has been minimized.

Copy link

commented Oct 7, 2016

It's obviously "break from named blocks" @vitiral not literally "break from any block", so no compatibility issues. I donnot if it's a good idea or not, just thought it might be worth asking.

@rfcbot

This comment has been minimized.

Copy link

commented Oct 11, 2016

It has been one week since all blocks to the FCP were resolved.

@aturon aturon merged commit b746484 into rust-lang:master Oct 22, 2016

@aturon

This comment has been minimized.

Copy link
Member

commented Oct 22, 2016

RFC has been merged!

Further discussion should happen on the tracking issue.

@rust-lang rust-lang locked and limited conversation to collaborators Oct 22, 2016

Sign up for free to subscribe to this conversation on GitHub. Already have an account? Sign in.
You can’t perform that action at this time.