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

RFC: label-break-value #2046

Merged
merged 7 commits into from Feb 27, 2018

Conversation

@ciphergoth
Copy link
Contributor

commented Jun 26, 2017

Rendered

Tracking issue: rust-lang/rust#48594

Allow a break not only out of loop, but of labelled blocks with no loop. Like loop, this break can carry a value.

See also Pre-RFC discussion.

Status as of 2018-01-18.

@ciphergoth ciphergoth changed the title label-break-value RFC RFC: label-break-value Jun 26, 2017

@mark-i-m

This comment has been minimized.

Copy link
Contributor

commented Jun 27, 2017

This feels to much like a goto. I don't really want Rust == Fortran 😛

@scottmcm

This comment has been minimized.

Copy link
Member

commented Jun 27, 2017

@mark-i-m Is there something particular about this proposal that makes it feel too goto-like?

This seems to have the same restrictions as the labeled break we already have, as well as to normal break and return: cannot go backwards, cannot skip variable definitions, can only exit a syntactically-visible {} block. (Or, more abstractly, that it doesn't violate the "progress of the process remains characterized by a single textual index" property from Go To Considered Harmful.)

@eddyb

This comment has been minimized.

Copy link
Member

commented Jun 27, 2017

The problems with goto are more or less those of safety. Rust doesn't have the same issues.
Besides you can already do this with loop { break {...} }, the difference is purely in ergonomics.

@mark-i-m

This comment has been minimized.

Copy link
Contributor

commented Jun 27, 2017

that it doesn't violate the "progress of the process remains characterized by a single textual index" property from Go To Considered Harmful

This is a fair point, but I feel more strongly, I guess.

I personally don't like the labeled breaks we already have. IMHO, they usually just make control flow harder to follow/write/debug without really providing any benefit, which is the heart of Dijkstra's point. In fact, I would go so far as to say, that I generally dislike normal break/continue, too, but I accept them for lack of a better alternative. The way I see it, the more entry/exit points you have for a loop, the worse code quality is -- it's just becomes convoluted to reason about loop invariants.

@mark-i-m

This comment has been minimized.

Copy link
Contributor

commented Jun 27, 2017

I should add that by breaking out of the middle of a block, you have effictively made it into 2 blocks, one of which doesn't always happen. And that is not always syntactically obvious, IMHO...

@ciphergoth

This comment has been minimized.

Copy link
Contributor Author

commented Jun 27, 2017

Full text of Go To Statement Considered Harmful - very interesting!

@ciphergoth

This comment has been minimized.

Copy link
Contributor Author

commented Jun 27, 2017

I filed a Pre-RFC about this, see also discussion there.

Delete superfluous paragraph
 link to discussions in discussions instead.
@ciphergoth

This comment has been minimized.

Copy link
Contributor Author

commented Jun 27, 2017

Other discussions: I proposed this here. An identical proposal was part of the explanation for trait based exception handling.

@golddranks

This comment has been minimized.

Copy link

commented Jun 27, 2017

I think that making control flow more flexible is generally a good thing. As already argued by others Dijkstra's critique doesn't apply here; the critique is against obfuscating the program state using surprising control paths. In the context of this feature it's only allowed to break outwards from a scope, which doesn't allow witnessing uninitialised variables or skipping in the middle of some code that expects there to be a state set up by the earlier code. It does allow skipping some code in a possibly "surprising way" the sense that one can jump from an inner scope to the grandparent scope, but unlike exceptions, this is still a local feature that is well visible in the local context – so in the end, it hardly isn't actually surprising, and when used with discretion, can lead to cleaner code.

I'd argue, that without flexible and safe control flow constructs, people tend to store the "which path" information to local flags or use inner functions to be able to return early. These both feel like hacks to me. The problem with manually juggling control flow flags is that the compiler can't hardly reason about their state and the problem with functions is that they are a wrong abstraction – they come with a new stack frame and aren't easily able to access the parent frame unless the state is explicitly passed to them. They are too heavyweight. Having labelled breaks is a nice way to retain the "state machine-y" feeling of a function-local control flow but still allow more flexible flow that comes handy from time to time.

@eddyb

This comment has been minimized.

Copy link
Member

commented Jun 27, 2017

@ciphergoth In fact my first instinct for desugaring catch is that there's a label on it (or at least the compiler has a way to refer to it) and ? uses break 'innermost_catch err instead of return err.
I haven't looked much into how it ended up being implemented but AFAIK it's close enough.
The advantage of a proposal like this is that you can go to one of many labels instead of just one.

@ciphergoth

This comment has been minimized.

Copy link
Contributor Author

commented Jun 27, 2017

@eddyb The RFC that proposes catch invents pretty much exactly what I propose here in order to describe what it does. It also describes return in terms of a break to a special 'fn scope for the whole function.

In the pre-RFC discussion, nikomatsakis says:

The compiler already internally supports [labelled blocks] for use with catch { } (that is how the HIR represents catch).

@withoutboats withoutboats added the T-lang label Jun 27, 2017

@mark-i-m

This comment has been minimized.

Copy link
Contributor

commented Jun 28, 2017

@golddranks

As already argued by others Dijkstra's critique doesn't apply here; the critique is against obfuscating the program state using surprising control paths.

Perhaps I will always just disagree on this... I suspect I am probably more extreme than most on this point. It looks like I am pretty outnumbered here, so I wont spam everyone more beyond this post, unless asked for more 😛

Basically, I can't imagine many useful situations where this is easier to follow

'block: {
    do_thing();
    if condition_not_met() {
        break 'block;
    }
    do_next_thing();
    if condition_not_met() {
        break 'block;
    }
    do_last_thing();
}

than this

do_thing();

if condition_met() {
    do_next_thing();
    if condition_met() {
        do_last_thing();
    }
}

In the first example it's not clear that the preconditions for do_last_thing are the conditions_met1() && conditions_met2(). It is also a bit annoying that syntactically non-obvious prefixes of a block might execute.

But in the second one, the curly braces (and formatting conventions) make it clear, which is what we expect because curly braces are the primary way rust indicates a block of code. Of course someone will argue that if you have 50 of them, you will have too much indenting. I thinks it's worth it, but I think that's really a matter of taste.

@est31

This comment has been minimized.

Copy link
Contributor

commented Jun 28, 2017

@mark-i-m In my use case, I need to conditionally break from inside a deeply nested structure, the eno! invocation is 4 layers inside structure, with loops and if's outside. This can't just be simply refactored to use if.

Also, I especially like the pattern to break early if some condition is not met, so that there is no big rightward drift.

@mark-i-m

This comment has been minimized.

Copy link
Contributor

commented Jun 29, 2017

@est31 I'll take your word for it that it is hard to refactor (I haven't tried). I guess I can see the use case, but I still don't really like the break as a pattern... although, I don't have an alternative, other than major refactoring...

@scottmcm

This comment has been minimized.

Copy link
Member

commented Jun 29, 2017

@mark-i-m I actually agree with your for that example. Overall, though, I suspect that people won't reach for this except in cases where if and match are awkward too, so it doesn't scare me to have it.

The example I found quite compelling was this one:

let result = 'block: {
    for &v in first_container.iter() {
        if v > 0 { break 'block v; }
    }
    for &v in second_container.iter() {
        if v < 0 { break 'block v; }
    }
    0
}

Because a simple translation to normal constructs ends up something like this:

let mut result = None;
for &v in first_container.iter() {
    if v > 0 {
        result = Some(v);
        break;
    }
}
if result.is_none() {
    for &v in second_container.iter() {
        if v < 0 {
            result = Some(v);
            break;
        }
    }
}
let result = result.unwrap_or(0);

Which I find more awkward, as it obscures the symmetry and has the compiler less able to help with the initialization logic. (You could also do this one with the {||{ … }}() "operator" and return, but I'm not a fan of immediately-called-closure as a pattern either.)

@ciphergoth

This comment has been minimized.

Copy link
Contributor Author

commented Jun 29, 2017

Should I be changing the examples in the RFC to reflect discussion here?

@mark-i-m

This comment has been minimized.

Copy link
Contributor

commented Jun 30, 2017

I think it would be useful to include some of the motivating examples. And I would also like to see the disadvantages section updated, with some of the objections, even if the language is not strongly worded...

Fix broken Rust code with proper references
Various other small improvements
@ciphergoth

This comment has been minimized.

Copy link
Contributor Author

commented Jul 1, 2017

For the specific example given you could also do this:

first_container.iter().filter(|&&v| v > 0).chain(
        second_container.iter().filter(|&&v| v < 0)
    ).map(|&v| v).next().unwrap_or(0);
@mark-i-m

This comment has been minimized.

Copy link
Contributor

commented Jul 1, 2017

@Ericson2314

This comment has been minimized.

Copy link
Contributor

commented Jul 3, 2017

Big fan of this; thanks for writing!

One nit is while the desugaring is correct, I don't think in long term we should implement/teach/think about the feature that way. I rather have:

  1. Break on block is fundamental

  2. Breaking out of loop is no different than breaking out of underlying block

  3. Break with no label is desugared to innermost loop instead of innermost block for historical reasons.

RFC language can be incremental I suppose, but o hope something like that makes the books.

@ciphergoth

This comment has been minimized.

Copy link
Contributor Author

commented Jul 3, 2017

Ericson2314 - how do we integrate continue into that teaching plan?

Maybe we could describe loops as being implicitly this:

'outer: {
    for i in container.iter() {
        'inner: {
            LOOP BODY
        }
    }
}

Then break means break 'outer and continue means break 'inner. continue 'label really means something like continue 'label_inner.

@alercah

This comment has been minimized.

Copy link
Contributor

commented Feb 21, 2018

(I should add: I'm not convinced the idea is better by any means. It was just idle musing.)

@rfcbot

This comment has been minimized.

Copy link

commented Feb 24, 2018

The final comment period is now complete.

@Centril Centril referenced this pull request Feb 27, 2018

Open

Tracking issue for RFC 2046, label-break-value #48594

2 of 4 tasks complete

@Centril Centril merged commit 417f92d into rust-lang:master Feb 27, 2018

@Centril

This comment has been minimized.

Copy link
Member

commented Feb 27, 2018

Huzzah! The RFC is merged!

Tracking issue: rust-lang/rust#48594

@GuillaumeGomez

This comment has been minimized.

Copy link
Member

commented Feb 28, 2018

So we just added goto in rust. I wish I saw this RFC sooner to comment against it but too late... :-/

@Centril

This comment has been minimized.

Copy link
Member

commented Feb 28, 2018

So we just added goto in rust.

Honestly, I don't think that is a fair description =)

@rpjohnst

This comment has been minimized.

Copy link

commented Feb 28, 2018

One of the reasons goto makes things harder is because it allows for irreducible control flow. Not only does this make reading the code harder for humans, but it makes the compiler's job more complicated.

Fortunately, label-break-value does not allow for irreducible control flow. It's hardly any different from early returns, just applied at the expression level.

@mark-i-m

This comment has been minimized.

Copy link
Contributor

commented Feb 28, 2018

It's hardly any different from early returns, just applied at the expression level.

Needless to say, not everyone agrees, but it is done already, and the debate has already been had...

@rpjohnst

This comment has been minimized.

Copy link

commented Feb 28, 2018

The point I'm making is not a matter of opinion- label-break-value does nothing to the control flow graph that early returns don't already do, especially when you consider inlining.

You can argue the subjective costs and benefits to humans of it all day long, which is why I thought I'd point out an objective way to look at it.

@est31

This comment has been minimized.

Copy link
Contributor

commented Feb 28, 2018

Previously, I was emulating the feature with a loop { /*code*/ if condition { break; } /*more code */ break; }. With labeled blocks I can simplify this to 'outer { /*code*/ if condition { break 'outer; } /*more code */ }. Neither of the two is goto, you can only jump back and scoping questions are immediately clear and resolved. I hate goto and wouldn't want it in the language, but this RFC is no goto.

@scottmcm

This comment has been minimized.

Copy link
Member

commented Feb 28, 2018

@GuillaumeGomez See related previous discussion above: #2046 (comment)

@SoniEx2

This comment has been minimized.

Copy link

commented Mar 1, 2018

if statements are glorified gotos. it doesn't matter what language they're in.

here's a lua example because I'm too lazy to write a rust example when I can use an existing example. https://gist.github.com/SoniEx2/fc5d3614614e4e3fe131/#file-special-lua-L11-L71

labeled blocks are just IIFEs on steroids.

@GuillaumeGomez

This comment has been minimized.

Copy link
Member

commented Mar 2, 2018

Yes I saw it, I commented after. I just don't like the feature, but it's nothing more than an opinion in a sea of opinions. I just wanted to comment and so did I. :)

@mark-i-m

This comment has been minimized.

Copy link
Contributor

commented Mar 2, 2018

I think the question is inherently a bit subjective... Does this encourage less-clear-than-it-could-be control flow? (as opposed to an objective question: does this fundamentally introduce new control flow?)

Anyway, I think that the best thing to do now would be to focus on making the feature as good as it can be 🥇

@SoniEx2

This comment has been minimized.

Copy link

commented Mar 2, 2018

Does this encourage less-clear-than-it-could-be control flow?

I mean, it allows replacing:

loop {
/* ... */
if cond { break val; }
/* ... */
break otherval;
}

with:

'block: {
/* ... */
if cond { break /*'block?*/ val; }
/* ... */
val
}

So I mean, if you find the former clearer... ;)

@derekdreery

This comment has been minimized.

Copy link

commented Jun 15, 2019

I'm a big fan of this idea for a number of reasons

  1. People are already doing this in hacky ways (loops that only ever run once etc.). I think this proves the need exists
  2. It's not goto. You can't jump arbitrarily around. goto is sometimes used like this in C, but IMO that's what it should be used for.
  3. It simplifies the language rather than complicating it. This is because it unifies concepts of early return and early ending of loops, so conceptually you can think of them as the same. I always like features that make the language simpler. (I accept this is partially subjective, hopefully I've put forward a convincing argument)
@Kimundi

This comment has been minimized.

Copy link
Member

commented Jun 16, 2019

I also recently had to use the loop hack, so I'd really like to see this feature in Rust.

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