-
Notifications
You must be signed in to change notification settings - Fork 1.6k
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
RFC: label-break-value #2046
Conversation
This feels to much like a goto. I don't really want Rust == Fortran 😛 |
@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 |
The problems with |
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. |
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... |
Full text of Go To Statement Considered Harmful - very interesting! |
I filed a Pre-RFC about this, see also discussion there. |
link to discussions in discussions instead.
Other discussions: I proposed this here. An identical proposal was part of the explanation for trait based exception handling. |
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. |
@ciphergoth In fact my first instinct for desugaring |
@eddyb The RFC that proposes In the pre-RFC discussion, nikomatsakis says:
|
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 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. |
@mark-i-m In my use case, I need to conditionally break from inside a deeply nested structure, the Also, I especially like the pattern to break early if some condition is not met, so that there is no big rightward drift. |
@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... |
@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 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 |
Should I be changing the examples in the RFC to reflect discussion here? |
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... |
Various other small improvements
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); |
I won't lie. I'm pretty sure most of these examples can be expressed more
elegantly with some refactoring, but it might take some effort...
…On Jul 1, 2017 2:05 PM, "Paul Crowley" ***@***.***> wrote:
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);
—
You are receiving this because you were mentioned.
Reply to this email directly, view it on GitHub
<#2046 (comment)>, or mute
the thread
<https://github.com/notifications/unsubscribe-auth/AIazwKQtyYLkV7CG1drVxECCQmN8cGJzks5sJop3gaJpZM4OFz9D>
.
|
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:
RFC language can be incremental I suppose, but o hope something like that makes the books. |
Ericson2314 - how do we integrate Maybe we could describe loops as being implicitly this: 'outer: {
for i in container.iter() {
'inner: {
LOOP BODY
}
}
} Then |
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 🥇 |
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... ;) |
I'm a big fan of this idea for a number of reasons
|
I also recently had to use the |
I would strongly prefer this to be permitted, and resolved by only binding labelled Something like this is needed for macros which want to adjust the control flow. I came across one in |
What you want can still be written as 'loop: loop {
'labelled_block: {
if condition() {
break 'loop;
}
}
} Would that meet your needs? |
No, sadly not. The Currently
expands to (roughly)
This is bad because it repeats the else clause and, worse, the different copies of it can pick up differnet variable bindings (imagine if there were an
But this does not work correctly if the macro user writes
According to the current RFC, this I don't think having it bypass the named blocks is at all strange or confusing. Perl's named blocks work the same way. This limitation is a real shame becaue there are a lot of cool things that can easily be done with macros that are able to define their own control flow like this, but they're lacking this primitive. You can't use the |
This limitation would force user crate to break explicitly. How is that not a good thing? If anything this limitation can be used by macros to prevent user break from conflicting with macro break! |
(You might think that the macro could "catch" the user's |
Do you want hygienic break? |
I'm not sure what you mean. The
Err, if the macro is supposed by its function to embed the user's code in a loop, then the user's
Essentially, yes. In fact the named break is already hygienic, I think. The labels are identifiers and I assume they are already hygienic so that if the macro and the user use the same name, it won't be treated as the same identifier. The only difficulty is that the "unnamed label" is not hygienic because it inserts itself unconditionally. |
Well, luckily wrapping the user code in a label-break-value causes it to error instead of miscompiling, thus alerting the user of how to fix it. That's a win, yeah? You don't make the unnamed label hygienic. You introduce a mechanism that can invalidate the unnamed label. That mechanism is called label-break-value. |
I think it might be worth arguing that There's definitely also an argument for hygiene of unlabeled This RFC is not really the right place to argue either, though, especially not in the now-closed RFC thread. This conversation should be on ILRO or Zulip. |
Looks like this predates the "Prior Art" section, but nitroll mentioned that emacs lisp has this, even using |
Rendered
Tracking issue: rust-lang/rust#48594
Allow a break not only out of
loop
, but of labelled blocks with no loop. Likeloop
, this break can carry a value.See also Pre-RFC discussion.
Status as of 2018-01-18.