This is a tracking issue for RFC 2046 (rust-lang/rfcs#2046).
Steps:
Unresolved questions:
return
as the keyword instead of break
? https://github.com/rust-lang/rfcs/pull/2046#issuecomment-357847755break
; call it resolved. https://github.com/rust-lang/rust/issues/48594#issuecomment-381390664Using "return" would have interesting implications for labeled ?
(tryop questionmark operator thingy).
Use return as the keyword instead of break?
@mark-i-m and @joshtriplett have already spoken out against return, but I'll join in given that it is still apparently an unresolved question.
break (and continue) are only usable with a loop.
If you can break something, you can continue it. (I don't think there's an obvious syntax to pick for continue on a block.)
In C, C++, Java, C#, JavaScript and probably more languages you are usually break
ing a switch statement in order to prevent fall-through. Rust has solved this much nicer with | in patterns but people coming from those languages won't really see break
as something only for for loops. Especially as Java and JavaScript expose the feature via break
as well and not return
.
The "rules to remember" argument works into the other direction really well however. From what I can tell it is a commonality of the mentioned languages as well as Rust that return only applies to functions and nothing else. So if you see a return, you know that a function is being left.
Labeling a block doesn't cause errors on existing unlabeled breaks
First, I think this happens very rarely as the labeled break feature is admittedly not something that will be used 10 times per 1000 lines. After all, it will only apply to unlabeled breaks that would cross the boundary of the block, not unlabeled breaks inside the block. Second, users of Rust are accustomed to complaints / error messages by the compiler, they will happily fix them! Third (this is the strongest point I think), if instead of labeling a block you wrap it into a loop, you already need to watch out for unlabeled breaks and there is no error message that conveniently lists the break statements, you've got to hunt for them yourself :).
Especially as Java and JavaScript expose the feature via break as well and not return.
This to me is the killer point. break from blocks is a thing in many languages. return from blocks...not so much.
Personally, I share @joshtriplett's view on using break
instead of return
, but it seemed to me like the discussion hadn't been resolved on the RFC... If you believe the question is resolved in the lang team, tick the box with a note =)
Just saying that I'm working on this. Don't need mentor instructions. Just to not duplicate any efforts. Expect a PR soon.
I'm still in favour of return
over break
, but I can agree to disagree here. Question resolved.
Currently (with rustc 1.28.0-nightly (a1d4a9503 2018-05-20)
) rustc does not allow unsafe
on a labeled block. Is this expected?
@topecongiro Yes, I believe it's intentional that this is currently allowed only on plain blocks. It might change in future, but given that this is such a low-level and unusual feature, I'm inclined towards that being a feature rather than a restriction. (On the extreme, I certainly don't want else 'a: {
.)
Definitely agree. Unsafe + unusual control flow sounds like something to discourage.
In a pinch, though, you could use:
'a: {
unsafe {...}
}
Right?
Actually, altho else does create a new lexical scope, it's not a block. The whole if-else is a block (kinda). So no, you wouldn't get else 'a: {
you'd get 'a: if ... else {
.
else
contains a block (expression). there is no "new lexical scope" without blocks.
An even worse surface syntax position than else
would be 'a: while foo 'b: {...}
.
(interestingly enough, continue 'a
is break 'b
, we might want to rely on that at least internally)
(interestingly enough,
continue 'a
isbreak 'b
, we might want to rely on that at least internally)
That's a great observation!
I think labels should be part of block-containing expressions, not blocks themselves. We already have precedent for this with loop
. (As it happens, a plain block itself is also a block-containing expression. But things like if
and loop
are block-containing expressions without being blocks I guess.)
(Things like while
or for
shouldn't support label-break-value, because they could or could not return a value based on whether they complete normally or with a break
.)
@eddyb
(interestingly enough, continue 'a is break 'b, we might want to rely on that at least internally)
Only if break 'b
re-checks the loop condition...
@mark-i-m It's equivalent to 'a: while foo {'b: {...}}
, the break
wouldn't check the loop condition, the loop itself would, because the loop condition is checked before each iteration of the body block.
Woah, I find that _highly_ unintuitive. I expect break 'b
to be basically goto 'b
, meaning we never exit the loop body and the condition is not checked again...
Oh :man_facepalming: I see...
This is why I don't like labeled break/continue :/
Well, we specifically don't have the ability to label these weird inner blocks, so I don't see the problem. break
always means "leave this block" and, given the above restriction, there's no way for that to mean anything other than "goto the spot after the associated closing brace."
My confusion was not specific to weird inner blocks, but I don't really want to reopen the discussion. That already happened and the community decided to add it.
Okay, I understand accessibility is a big issue with programming languages... however, labeled break is extremely useful if you write code like me.
So, how can we make labeled break more accessible?
So, how can we make labeled break more accessible?
That's a great question. Some ideas I had:
As a first (admittedly biased) sample, my last (and first) encounter with labeled break in real code was not stellar: https://github.com/rust-lang/rust/pull/48456/files#diff-3ac60df36be32d72842bf5351fc2bb1dL51. I respectfully suggest that if the original author had put in slightly more effort they could have avoided using labeled break in that case altogether... This is an example of the sort of practice I would like to discourage if possible.
That's... not labeled break?
Regarding how break
and continue
desugar into this, that was also mentioned in the original RFC discussion by the RFC's author; see https://github.com/rust-lang/rfcs/pull/2046#issuecomment-312680877
I suppose the name break
is suboptimal, but it is well-established for loops. A more "principled" approach would be to use the syntax return 'a value
, which immediately continues execution "after" the block 'a
, using the value value
for that block.
@mark-i-m "not using a feature because it's not accessible" isn't "making said feature accessible".
How can we tweak labeled break so I get the full expressive power of the language while at the same time you stop complaining that your brain can't process flow control things as well as the compiler?
(Also, the code you linked doesn't seem to do with RFC 2046/label-break-value... did you perhaps link the wrong code?)
That's... not labeled break?
(Also, the code you linked doesn't seem to do with RFC 2046/label-break-value... did you perhaps link the wrong code?)
That's true, it was a normal labeled continue before I changed it, but I think the same problems exist (and are possibly worse since the control flow of the rest of a routine may be affected by what value you return).
@mark-i-m "not using a feature because it's not accessible" isn't "making said feature accessible".
How can we tweak labeled break so I get the full expressive power of the language while at the same time you stop complaining that your brain can't process flow control things as well as the compiler?
Sorry, I don't mean to complain. If I am the only person who feels this way, I don't mind stepping aside.
IMHO, this is a fundamental problem with labeled break/continue: it's _too_ expressive, and the only way I know to mitigate it is to have recommended usage as "good style" (whatever that means). For example, "only use labeled break with value from the beginning or end of a loop body (not the middle)." This would mean that possible ways to exit a loop with a value are easy to spot and reason about.
This is how I avoid goto/labeled break in other languages: https://gist.github.com/SoniEx2/fc5d3614614e4e3fe131/#file-special-lua-L4-L72
Is that more readable?
If so, perhaps we can figure out some sort of conditional system based on labeled blocks. Similar to this, maybe.
The point of unlabeled break and continue is to provide for those cases where you can't easily put the condition/value in the loop header. Some loops are simply far more straightforward, readable, fast, etc. with the break in the middle.
The point of labeled break and continue in loops is similar- sometimes the only alternative is to introduce a new variable, a new function (thereby abusing return
), or some other contortion that only makes things harder to follow, however unfortunately-convoluted a labeled break may be.
But those two features are not what this thread is even about. They're fairly universal, precisely because of the improvements they offer for expressing inherently-complex control flow. This thread is instead about breaking out of a non-loop block. This is certainly more novel, and people may not know to look for a break
outside of a loop, though requiring a label means the signal is still there once you know what it means.
This is what I meant about your example, @mark-i-m- it was a fairly standard use of a labeled loop, rather than a labeled block.
A more "principled" approach would be to use the syntax return 'a value, which immediately continues execution "after" the block 'a, using the value value for that block.
Side note about break
vs return
: I prefer break
because it's static control flow. return
is dynamic, in that it goes back to the caller, which may be anywhere at all. It means "I've completed my responsibility, there's nowhere else to look to see what I do." break
always goes somewhere that's lexically in scope, just as with loops.
I think return 'label expr
reads really well from a "do what I say" perspective in that it can be thought of as "return expr to the location 'label". I don't think break 'label expr
reads equally well in this perspective...
In isolation from other programming languages, I might have therefore advocated return 'label expr
. However, given control flow in other languages, reusing return
becomes suddenly a much less viable option and this sways me in favor of break 'label expr
.
I'm firmly of the opinion that it should be break 'label expr
and not return 'label expr
. Doing otherwise would be totally inconsistent with our existing usage of break 'label
in loops.
@SoniEx2 I think I do prefer the snippet you posted, largely because the variables are a good way of documenting loop invariants. OTOH, It might be possible to do the same with the names of labels (i.e. any time this labeled block is entered, invariant P holds). I guess it this is a place where it would be good to have a few more code samples from the wild...
The feature was implemented in https://github.com/rust-lang/rust/pull/50045 by @est31 and has been in nightly since 2018-05-16 (+17 weeks) and so it has baked enough for stabilization. Furthermore, there have been no issues reported since the PR that implemented this was merged. All unresolved questions have also been resolved since, and there is strong consensus that break
should be the surface syntax instead of return
.
Thus, I move to stabilize label-break-value (RFC 2046).
@rfcbot merge
@rfcbot concern FIXME-in-tests
The last test file currently has a FIXME:
// FIXME: ensure that labeled blocks work if produced by macros and in match arms
This should be resolved before stabilizing.
I wrote some tests to check that the expected behavior wrt. the FIXME is actually implemented:
// run-pass
#![feature(label_break_value)]
#[test]
fn lbv_match_test() {
fn test(c: u8, xe: u8, ye: i8) {
let mut x = 0;
let y = 'a: {
match c {
0 => break 'a 0,
v if { if v % 2 == 0 { break 'a 1; }; v % 3 == 0 } => { x += 1; },
v if { 'b: { break 'b v == 5; } } => { x = 41; },
_ => {
'b: {
break 'b ();
}
},
}
x += 1;
-1
};
assert_eq!(x, xe);
assert_eq!(y, ye);
}
test(0, 0, 0);
test(1, 1, -1);
test(2, 0, 1);
test(3, 2, -1);
test(5, 42, -1);
test(7, 1, -1);
}
#[test]
fn lbv_macro_test() {
macro_rules! mac1 {
($target:lifetime, $val:expr) => {
break $target $val;
};
}
let x: u8 = 'a: {
'b: {
mac1!('b, 1);
};
0
};
assert_eq!(x, 0);
let x: u8 = 'a: {
'b: {
if true {
mac1!('a, 1);
}
};
0
};
assert_eq!(x, 1);
}
// compile-fail
#![feature(label_break_value)]
fn lbv_macro_test_hygiene_respected() {
macro_rules! mac2 {
($val:expr) => {
break 'a $val;
};
}
let x: u8 = 'a: {
'b: {
if true {
mac2!(2);
}
};
0
};
assert_eq!(x, 2);
macro_rules! mac3 {
($val:expr) => {
'a: {
$val
}
};
}
let x: u8 = mac3!('b: {
if true {
break 'a 3;
}
0
});
assert_eq!(x, 3);
let x: u8 = mac3!(break 'a 4);
assert_eq!(x, 4);
}
Tests similar to these should be added before we move from proposed-FCP to FCP.
Team member @Centril has proposed to merge this. The next step is review by the rest of the tagged teams:
Concerns:
Once a majority of reviewers approve (and none object), this will enter its final comment period. If you spot a major issue that hasn't been raised at any point in this process, please speak up!
See this document for info about what commands tagged team members can give me.
@rfcbot concern cost-benefit
From the RFC FCP proposal:
Another group felt that the "cost benefit" of this feature to the language didn't quite make the cut. In other words, the increased complexity that the feature would bring -- put another way, the possibility that people would actually use it and you'd have to read their code, I suppose, as well as the overall size of the language -- wasn't commensurate with its utility.
I still don't think we should have this feature at all. Since it's been implemented has it been used a lot? In the cases that it has been used is it significantly worse to use functions? If there is a benefit here, does it outweigh the cost of making the language larger and more complex?
@nrc I share the same concern. I do understand the argument for having it available so that macros can use it, but at the same time, I'd much rather not have this at all.
I don't want to retread arguments from the original RFC thread, but I do think that this is a reasonable point to ask about experiences with this feature. What uses has it seen?
hand-written parsers, mostly... (I like my hand-written parsers >.<)
would be more useful/easier to use with labeled-propagate (try_foo() 'bar?
).
@rfcbot concern use-cases
Summarizing some discussion on Discord: We'd like to see concrete use cases for this, from actual code, that don't come across more clearly when rewritten to not use this feature.
~FWIW, my implementation of EXPR is PAT
relies on break 'label value
being available at least in AST, I don't know how the desugaring could work without it.
Implementation of EXPR?
in the compiler relies on break 'label value
as well.~
~It's some kind of a basic building block for larger control flow features, so it may be needed to be implemented in the compiler anyway.
So the "cost" here is probably just making it available in surface syntax.~
EDIT: I've totally misinterpreted the issue, I though this is about loop { ... break 'label value ... }
, not block { ... break 'label value ... }
.
I never had a chance to try this one because I always forget that it's already implemented.
@petrochenkov Talking to @joshtriplett on Discord, they pointed out they were worried about user-facing complexity, not the implementation of the language.
I think the complexity increase is minimal: for readers, it should be obvious what this means because the concept already exists in loops, etc.
I'd otherwise have to use a loop with an unconditional break statement, which is less clear and in fact there is even a clippy lint (never_loop) about this. So I think that there is a benefit.
As for the use cases, that topic has come up in the RFC already. I pointed out my use case here. Also see the use case listed by @scottmcm direcly below. Maybe there are more in the thread, idk. @joshtriplett does that resolve the use case question?
I agree with @nrc and @joshtriplett & I also want to raise a process concern here: we tentatively accepted this RFC with an explicit caveat that stabilization was blocked on revisiting the questions that @nrc and @joshtriplett have raised, but @Centril's merge proposal doesn't mention this blocking concern at all, and treats this as a very standard "feature has baked" merge. I'm not blaming @Centril for this, but a process breakdown: if we're going to accept features tentatively with unresolved blockers on stabilization, we need to track those blockers.
It was concerning to me in terms of our entire processes to see that this went 2 and a half days without the blocking concern being brought up, and with most team members having already checked their box. Its conceivably, since we no longer require active consensus of all members, that this could have entered FCP without the blocker even being raised. This feels like a subversion of the prior agreement which led me to consense with merging the RFC, and I think its entirely caused by insufficient tracking of information.
@withoutboats Yes, exactly. This makes me rather less inclined, in the future, to accept things on a "we'll XYZ during the stabilization process" basis, until we have some process in place that makes it exceedingly unlikely that'll be missed.
@withoutboats
I'm not blaming @Centril for this, but a process breakdown: if we're going to accept features tentatively with unresolved blockers on stabilization, we need to track those blockers.
Apologies nonetheless; I was unaware of the caveat, but I should have checked for such things nonetheless when I created the tracking issue.
This feels like a subversion of the prior agreement which led me to consense with merging the RFC, and I think its entirely caused by insufficient tracking of information.
I should have made an unresolved question for it; I believe the process mistake happened at that point.
As for how to improve the process; I think it is important that unresolved questions make it to the _text_ of the RFC; it becomes much harder to find them otherwise ;)
As for the fcp-merge proposal; I personally do think it would be useful for reasons of uniformity and for use by macros. However, if you believe it is too early to propose stabilization; feel free to cancel the proposal :)
@est31
I'd otherwise have to use a loop with an unconditional break statement,
Or restructure the code to avoid either.
I pointed out my use case here.
Why not replace the break 'pseudo_return
with return Ok(vectors);
?
as I've mentioned here, this is useful for hand-written parsers (even without labeled-propagate (try_foo() 'bar?
)).
label-break-value allows the easy imperativeization of otherwise functional code. generally, imperative code tends to be more readable than functional code.
Or restructure the code to avoid either.
Of course, Rust is turing complete. But restructuring might be a bit difficult. Overall, you can reject almost every sugar feature (and this is sugar feature) on the basis "you can just use the existing ways".
Why not replace the break 'pseudo_return with return Ok(vectors);?
Actually in this instance you are right, one can replace the break with a return Ok. But in other instances you might want to do processing afterwards, etc. The borrow checker works poorly across function boundaries, you can't factor every single such block into a function.
Anyways, I've broken my silence about commenting on language features through official means, and tbh I'm regretting it. All the same points, rehashed over and over again. This shit is a waste of my time, sorry. So don't expect any further comments from me.
@est31 I really appreciate you providing details; thank you.
There's an accessibility issue to testing and using these things, due to the requirement of nightly Rust.
I target stable. I hope this can be solved some day.
If we want use cases, here's one I hit a while ago; basically an elaboration of @est31's "do processing afterwards": I wrote a lexer that handles C++ string literal prefixes (in the actual C++ case, a combinatorial explosion of {L
, u8
, u
, U
, }{R
, }), and multi-byte tokens that with "gaps" in the number of bytes used (not sure that one makes sense without the example). The get_token
function currently looks like this:
fn get_token(&mut self) -> Token {
match decode_byte(self.source) {
// ...
// repeat four times with small variations for b'U', b'L', and b'R':
Some((b'u', rest)) => match decode_byte(rest) {
Some((b'"', rest)) => self.string_literal(Utf16String, rest),
Some((b'\'', rest)) => self.char_constant(Utf16Char, rest),
Some((b'R', rest)) => match decode_byte(rest) {
Some((b'"', rest)) => self.raw_string_literal(Utf16String, rest),
_ => self.identifier(rest),
},
Some((b'8', rest)) => match decode_byte(rest) {
Some((b'"', rest)) => self.string_literal(Utf8String, rest),
Some((b'\'', rest)) => self.char_constant(Utf8Char, rest),
Some((b'R', rest)) => match decode_byte(rest) {
Some((b'"', rest)) => self.raw_string_literal(Utf8String, rest),
_ => self.identifier(rest),
},
_ => self.identifier(rest),
},
_ => self.identifier(rest),
},
// ...
// the "gap" mentioned above is here: single-byte '.' and triple-byte '...' but no double-byte '..':
Some((b'.', rest)) => match decode_byte(rest) {
Some((b'0'..=b'9', rest)) => self.number(rest),
// note the _inner to avoid shadowing the outer `rest` used by the inner `Dot` case:
Some((b'.', rest_inner)) => match decode_byte(rest_inner) {
Some((b'.', rest)) => self.make_token(Ellipsis, rest),
_ => self.make_token(Dot, rest),
},
_ => self.make_token(Dot, rest),
},
// ...
}
}
Notice the pyramids of _ => self.identifier(rest)
s (repeated four times for u
, U
, R
, and L
) and _ => self.make_token(Dot, rest)
s, forming a kind of continuation-passing style where identifier
, string_literal
, etc. all must also call make_token
.
I would have liked to consolidate things back to a less continuation-y style using break
-from-block, and almost did so via labeled loop
s, but considered that version too weird to read. To be more specific:
make_token
calls to a single location after the main match decode_byte(self.source)
, and inlined it- it's small and contains unsafe
with its invariants upheld by get_token
.break 'label self.string_literal(..)
to short-circuit once finding a "
or '
, and then combined all the self.identifier(..)
calls to the end of that match arm.u
/u8
/U
/L
, then check for R
. This uses fewer break 'label
s, but still a handful.break 'label (Ellipsis, rest)
to short-circuit once finding a ...
, and then combined both (Dot, rest)
s to the end of that match arm.On the whole, this is basically the "flatten control flow with if + early return," without the requirement of extracting things into a separate function. That's extremely valuable in this case for a few reasons:
I would write out the whole thing but I guess I never committed that attempt (probably because I ran into all the problems I listed above) and I've spent enough words here already. :)
@SergioBenitez could you elaborate on http://rocket.rs/ 's usage of label_break_value
and your views on stabilization?
@Centril Sure! Here's the gist:
fn transform(request: &Request, data: Data) -> Transform<Outcome<_, _>> {
let outcome = 'o: {
if !request.content_type().map_or(false, |ct| ct.is_form()) {
break 'o Forward(data);
}
let mut form_string = String::with_capacity(min(4096, LIMIT) as usize);
if let Err(e) = data.read_to_string(&mut form_string) {
break 'o Failure(FormDataError::Io(e));
}
Success(form_string)
};
Transform::Borrowed(outcome)
}
Using this feature, I avoid:
Transform::Borrowed
to every "return" value in the block.Transform
is returned in different cases.I was pretty happy to see that this existed. This is exactly what I want to write. That being said, I can clearly write this differently to not depend on this feature.
@SergioBenitez Thanks! I wonder if you could (eventually) rewrite this with try { .. }
? Like so:
fn transform(request: &Request, data: Data) -> Transform<Outcome<_, _>> {
Transform::Borrowed(try {
if !request.content_type().map_or(false, |ct| ct.is_form()) {
Forward(data)?;
}
let mut form_string = String::with_capacity(min(4096, LIMIT) as usize);
if let Err(e) = data.read_to_string(&mut form_string) {
Failure(FormDataError::Io(e))?;
}
form_string
})
}
@Centril Yeah, that would work as long as there's only one success path, which is the case here.
Or restructure the code to avoid either.
By doing this, you run the risk of adding additional branches or subroutine calls that cannot be resolved by the optimizer. To me, such a control flow transformation seems to be a fairly complicated task.
In the cases that it has been used is it significantly worse to use functions? If there is a benefit here, does it outweigh the cost of making the language larger and more complex?
A small function might be inlined, but nevertheless, you would gain unnecessary code bloat or say, silly code duplications.
I would prefer the most elegant binary code possible. You can do so by a little more advanced control flow, almost the same as an early return from a function.
I wonder if you could (eventually) rewrite this with try { .. }? Like so:
[...]
That looks a little bit confusing, as branches are introduced, just to be optimized away. But in some situations one might need both. Thus, having
'a: {try{...}}
or
'a: try {...}
would be nice.
We'd like to see concrete use cases for this, from actual code, that don't come across more clearly when rewritten to not use this feature.
Please feel free to disgrace me:
It might not be the best code, but I wanted things getting to work at all.
I would like to experimentally restate the main loop as a state machine in terms of continuation passing style. But continuations and enforced tail calls are another topic.
Dumping some uses I found in the wild:
It would be very nice to see this feature stabilised. I have numerous use cases in which this syntax simplifies and clarifies intent significantly. For long sections involving performing fallible operations and unwrapping their results, this syntax is superb.
I agree with @zesterer that stabilization of this feature would be a benefit to the ecosystem and make certain patterns less annoying to write :+1:
FWIW, I recently used it in the compiler for the first time.
The pattern is the same as in the previous examples - we are checking multiple conditions and stop doing what we are doing if any of them is true.
https://github.com/rust-lang/rust/blob/21f26849506c141a6760532ca5bdfd8345247fdb/src/librustc_resolve/macros.rs#L955-L987
@erickt also wrote some code that wanted to use this for the same reason: checking multiple conditions, break out once one of them becomes false. You can fake this with immediately-called-closures or let _: Option<()> = try { ... None?; .. };
but both are pretty hacky.
@withoutboats @nrc @joshtriplett Do you have any thoughts on the use cases brought forth thus far by various people?
Ping @withoutboats @nrc @joshtriplett :) -- last lang meeting we discussed the possibility that y'all might try to rewrite some of the examples given above and show us how you'd structure them.
I felt spurred by greydon's blog post to write a comment here about why I don't want to stabilize this feature. I feel that its been a bit unfair of me to have agreed to this experiment; its hard to prove a negative but I don't have any idea what sort of code sample could overcome my opposition to adding this form of control flow to the language.
My overall objection is that I simply think Rust does not need more open-ended branching control flow constructs. We've got match and loop, and then on top of those algebraic data types, syntax sugar, function abstraction, and macros to create a huge array of control flow patterns for any ordinary user to handle. This already feels like it veers into overwhelming, and I personally have had to adopt some rules to manage the complexity of the decision tree (for example, I have a rule never to reach for combinators as a first pass: only if its obvious after the fact it would be nicer as combinators).
I would like to avoid adding more broadly applicable, highly flexible choices to Rust's control flow tool box. I'm only interested in adding control flow that is targeted at specific, important use cases, and has a high ergonomics impact. This construct in my opinion has the exactly inverted attributes: it is widely applicable, extremely maleable, and only mildly more convenient than the alternatives.
Moreover, I think this feature has another really negative quality: irregularity due to backwards compatibility. It is very irregular for break
to break out of blocks only if they're labeled, when it breaks out of unlabeled loops. This makes the feature harder to understand than it would be if it were regular, exacerbating the negative attributes exactly where I think they are the worst - comprehensibility.
I also think there are a lot of far more important features to focus on, and since we have a clear divide on this I would rather just postpone any consideration of it rather than trying to go through a long consensus process on this proposal.
I'd think the right thing to do is find every crate that uses this feature and rewrite the code so as to not use this feature, then.
@withoutboats Thank you for very effectively articulating many of the same objections I hold as well.
I'm going to go ahead and be bold here:
@rfcbot cancel
@rfcbot postpone
@joshtriplett proposal cancelled.
Team member @joshtriplett has proposed to postpone this. The next step is review by the rest of the tagged teams:
No concerns currently listed.
Once a majority of reviewers approve (and none object), this will enter its final comment period. If you spot a major issue that hasn't been raised at any point in this process, please speak up!
See this document for info about what commands tagged team members can give me.
@withoutboats
I would like to avoid adding more broadly applicable, highly flexible choices to Rust's control flow tool box. I'm only interested in adding control flow that is targeted at specific, important use cases, and has a high ergonomics impact.
I agree with this principle, and I think this feature meets that bar (familiar control flow targeted at specific, important use-cases). There are a number of times when I've reviewed or written code like the examples provided above where I feel that this is by far the cleanest and easiest-to-read way to write this code. Reusing existing control-flow constructs like loop
with an unconditional break
at the end misleads the user, and immediately-applied functions are often insufficient due to use of ?
, await!
, or other intermediate control-flow constructs.
Using loop
+ trailing break
is confusing, and we should prefer instead to have users state their real intention over requiring them to abuse a tool that was made for a different style of control-flow. This is similar to the fact that we even have a loop
construct, while other languages are content with while true { ... }
. Doing this makes it possible to write code that expresses a more clear intent and is more readable as a result.
In addition, this feature is something that I've always expected Rust to have, given that we have labeled-most-other-things and break-from-labeled-things, the fact that you cannot label or break a block seems confusing and wrong to me.
TL;DR: I think this feature is supporting real-world use-cases that can only be written otherwise through heavily-nested-if
statements or by abusing other constructs like loop-break-value. It's a small addition to the surface syntax that makes blocks behave as I would expect them to behave and allows me to write the code I mean rather than something much hackier.
@cramertj Sorry, I’m a bit confused. It sounds like you and @withoutboats /@joshtriplett are saying the exact opposite of each other?
(fwiw, I agree with @withoutboats /@joshtriplett)
@mark-i-m I don't agree with them that this feature should be closed. I think it should be merged (as indicated by my comments and marked checkbox above).
I agree with @withoutboats that we shouldn't add new unfamiliar tools that aren't motivated by specific, important use-cases. I think this feature will feel familiar and is motivated by specific, important use-cases.
@withoutboats
I feel that its been a bit unfair of me to have agreed to this experiment; its hard to prove a negative but I don't have any idea what sort of code sample could overcome my opposition to adding this form of control flow to the language.
I don't think the bar we set was "show us that no code could exist that could be better written with label-break-value"-- it's "show us that the specific code we want label-break-value for could be more clearly written another way." This conversation started when you and others asked for motivating examples of where this feature is useful, and many people on this thread (including myself) have provided examples. They weren't convincing to you or @joshtriplett, so I'm now asking how you both would write these examples without label-break-value. Do you agree that the examples are better written using label-break-value? If so, is your position that they're not common enough to outweigh the cost of allowing users to write potentially complicated break-to-block code?
I'm going to go ahead and be bold here:
@rfcbot cancel
@scottmcm proposal cancelled.
It is very irregular for break to break out of blocks only if they're labeled, when it breaks out of unlabeled loops.
Not sure whether it was mentioned in the thread or not, but blocks being untargetable by break;
makes labeled blocks qualitatively more powerful than labeled loop
s in some sense.
With labeled blocks you can make a control flow macro that supports break;
s inside of it, with the macro infra being completely invisible to the user.
With labeled loops there's always a risk that label-less break;
will be used and will be caught by the macro infra rather than by the intended target.
In my EXPR is PAT
prototype I used usual loop
s for desugaring initially, but things broke due to the aforementioned issue, I couldn't bootstrap the compiler in particular.
Labeled blocks wasn't implemented back then, so I had to introduce a special "untargetable loop
" into AST and use it in the desugaring.
As indicated by my checkbox, I am in favor of stabilizing this right now. I think this is a natural extension of the language that facilitates macros as well as control flow that does not so easily fit functional patterns. Even tho NLL makes lifetimes non-lexical, I think that being able to annotate blocks with lifetimes also strikes me as pedagogically helpful much like type ascription does.
However, since consensus has been hard to achieve, in the interest of finding it, I would suggest that we try to speed up work on try { ... }
as well as speeding up work on experimenting with https://github.com/rust-lang/rust/issues/53667 (or concurrently with @petrochenkov's EXPR is PAT
syntax).
Sorry, what is the status of this now? Are we in FCP or not?
@mark-i-m Since none of the labels proposed-final-comment-period
or final-comment-period
are on the issue, we are not in FCP or a proposal for one.
(and we have never been in FCP, though there was an initial proposal to enter FCP with all but three boxes checked)
@scottmcm
I'm going to go ahead and be bold here:
This repetition of @joshtriplett's wording I read as very sarcastic. As a member of the lang team, please be thoughtful in how you communicate with other contributors.
I am persuaded that this is, in fact, a good idea. I have been very anti- this feature previously, and I still don't think this is a feature that should ever be used by programmers. The use cases above are somewhat persuasive, but I think they might still be better factored into smaller functions. The case I found really persuasive is as the output from macros. If the user can insert return
some how, then this precludes translating into functions. However, what we really want is just plain goto
. I wonder if the 'big' solution is macros which can output HIR, rather than tokens, but that is a bit out there. In the meantime, it would be nice to have this feature for macro authors, so on balance I think we should stabilise.
Has anyone tried rewriting the things in a slower, less-intuitive and less-ergonomic way?
Breaking blocks is like ergonomic goto. It has just about none of the issues of goto, but is just as powerful.
Based on @nrc's change of heart, I move, yet again, to stabilize label_break_value
.
My report can be found in https://github.com/rust-lang/rust/issues/48594#issuecomment-421625182.
@rfcbot merge
@rfcbot concern FIXME-in-tests
To ensure that @joshtriplett and @withoutboats have time to raise any concerns they might still have, I'll record such a concern. I'll lift it once they tell me that they have no such concerns or when one of them has raised their own concern.
@rfcbot concern give-boats-and-josh-time-to-raise-any-concerns-they-might-still-have
As a process note for this issue and for all others, please avoid cancelling proposals back and forth... if you don't think something should move forward, just use concerns for that.
Team member @Centril has proposed to merge this. The next step is review by the rest of the tagged team members:
Concerns:
Once a majority of reviewers approve (and at most 2 approvals are outstanding), this will enter its final comment period. If you spot a major issue that hasn't been raised at any point in this process, please speak up!
See this document for info about what commands tagged team members can give me.
@rfcbot concern blocking
I've already expressed my position, which is that Rust should not have this feature. I doubt this will be a high enough priority for the project that I can devote the time to engaging in a consensus-reaching process on this proposal any time in the near future.
@rfcbot resolve give-boats-and-josh-time-to-raise-any-concerns-they-might-still-have
If it matters at all: Zig also has this feature.
On January 5, 2019 9:18:29 AM PST, Mazdak Farrokhzad notifications@github.com wrote:
@rfcbot resolve
give-boats-and-josh-time-to-raise-any-concerns-they-might-still-have
We've raised concerns already, and they amount to "this shouldn't be in the language". Those concerns aren't going away.
I prefer nrc's notion that macros should be able to generate IR. That seems like a completely reasonable solution for this and many future possibilities, without necessarily adding to the surface syntax of the language.
@joshtriplett
We've raised concerns already, and they amount to "this shouldn't be in the language". Those concerns aren't going away.
To be clear I meant registering them with @rfcbot
because the bot won't see old concerns registered after a proposal has been canceled. I raised that concern because I know that you have concerns, as a courtesy to you.
I prefer nrc's notion that macros should be able to generate IR. That seems like a completely reasonable solution for this and many future possibilities, without necessarily adding to the surface syntax of the language.
I have no idea what that would look like or that it is a better idea; it seems rather speculative (in the sense of "this might take years before a design is even agreed to") and more costly than the rather low-cost solution of break 'label expr
which also has benefits beyond being the result of macro expansion.
_As a process note for this issue and for all others, please avoid cancelling proposals back and forth... if you don't think something should move forward, just use concerns for that._
I certainly agree with that. However, I don't think a concern is the appropriate mechanism here. A concern seems like "if this were resolved then I'd approve". Here, the issue is "this shouldn't go forward at all, 'merge' is the wrong target, I'd like to 'close' instead". That doesn't seem best handled by the mechanism of a "concern".
@joshtriplett As a point of interest, could you direct me to the concerns that were raised that you mentioned previously? I've gone through the original RFC thread and am unsure what is being specifically referred to. Thanks.
@rfcbot concern ergonomics and optimization/performance (non-rust example of reduced performance and ergonomics https://gist.github.com/SoniEx2/fc5d3614614e4e3fe131#file-special-lua )
idk if I'm using this right
(note how some of the "desugaring" (actually I'd say it's quite the opposite of desugaring - goto is a primitive, loops desugar into goto) is quite awful)
(also note that lua lacks labeled anything so it forces you to use goto. even then, I believe at least one break-block would still be required, but probably with cleaner "desugaring".)
@rfcbot concern should-close-not-merge
(As suggested by @centril on Discord.)
I'm not sure if this is an argument in favour of stabilisation or against, but note that this is trivially easy to encode in stable Rust today:
fn main() {
'foo: for _ in 0..1 {
println!("break");
break 'foo;
println!("broken");
}
}
So on the one hand, we should just stabilise this because it is barely growing the language, just eliding for _ in 0..1
, on the other hand we should not stabilise this because there is an easy way to do it today (for when it is really necessary) and we should not encourage such an anti-pattern be used.
it seems kinda confusing and the label is unnecessary.
breaking blocks is a lot less confusing and requires the label.
I don't understand why you think breaking blocks is an anti-pattern. what applies to X doesn't necessarily apply to not-quite-X. I may be able to buy a computer for $299.99, but not $299.98, even tho they're "basically" the same.
@nrc
I'm not sure if this is an argument in favour of stabilisation or against, but note that this is trivially easy to encode in stable Rust today:
'foo: for _ in 0..1 { break 'foo; }
So on the one hand, we should just stabilise this because it is barely growing the language, just eliding
for _ in 0..1
,
'a: for _ in 0..1 { break 'a }
might work if the explicit goal is to not have anyone ever write 'a: { ... break 'a e; ... }
; however, it has the drawback of generating garbage IR that the type checker, MIR, and LLVM must process thus worsening compile times (at least in comparison to LBV).
on the other hand we should not stabilise this because there is an easy way to do it today (for when it is really necessary) and we should not encourage such an anti-pattern be used.
I think we disagree that LBV is an anti-pattern. At the end of the day we chose to imbue Rust with imperative control flow rather than just having Rust be a functional programming language. While I might have preferred to not have such control flow, it's fait accompli. The question is then whether LBV is somehow more unreadable or problematic than other imperative mechanisms in Rust. I don't think it is.
While I think that functions should be kept short (vertically) and shallow (indentation), it's better to be long and shallow than long and deep. I find that LBV helps avoid depth in some of the more complicated flows. LBV also seems readable to me as it explicitly denotes where it will jump to, making the flow well understood. All in all, I find LBV to be one of the least problematic imperative control flows.
'a: for _ in 0..1 { break 'a }
'a: { ... break 'a e; ... }
these may look similar, but they're not quite the same. while the first is arguably an anti-pattern, the second is arguably not.
kinda like loop {}
vs while true {}
(granted loop
can return a value while while
cannot, but let's ignore that for a bit)
@nrc That doesn't have quite the same behavior though: https://github.com/rust-lang/rust/issues/48594#issuecomment-450246249
In particular, the behavior of break;
and continue;
is different. Consider this hypothetical code:
some_macro! {
...
break;
...
}
If some_macro
expands to 'a { ... }
, that has different behavior than if it expands to 'a: for _ 0..1 { ... }
@SoniEx2
Has anyone tried rewriting the things in a slower, less-intuitive and less-ergonomic way?
Please try to communicate your concerns in a more positive and productive way. Mocking others doesn't get us anywhere, and makes people feel bad. We're all here because we want to make sure that Rust is the best programming language it can be, and part of that process is ensuring that the community is as positive and encouraging as possible.
Perhaps I'm falling foul of some big misunderstanding. I won't pretend to be an expert in LLVM, Rust's internals, or similar such things. That said, I do have some rudimentary experience with compiler design and I'm confused as to what the concern is. If someone could explain things, I'd really appreciate that.
The way I see things:
1) This doesn't change the ergonomics of flow control. There already exists constructs like this in Rust such as 'a: loop { if x { break 'a; } break; }
.
2) This doesn't particularly change the ergonomics of block returns: loops are already capable of returning values.
To me, this seems more like the intuitive completion of a set of features that Rust already has - generalizing those features for all (or at least more) blocks.
In terms of concerns about this being too similar to goto
, I'm further confused. This doesn't add anything that isn't already possible in Rust, and it doesn't permit backwards jumps (the primary problem that results in poor use of goto
regressing into spaghetti code), so I'm failing to understand what ramifications this feature might have in codegen since this seems to effectively be just syntax sugar for an always-breaking loop.
Could anybody explain to me in more specific terms what problems exist?
Procedurally, IMO it isn't appropriate to file concern blocking
or concern should-close-not-merge
-style concerns: this concern doesn't list a problem with the feature as-proposed or anything that we could work towards resolving-- it's just a perma-blocker. I don't think that rfcbot's concern mechanism should be used to perma-block features, but only to help track resolution of concerns and make sure that they are addressed / resolved appropriately. My understanding of the process was that the intent was to use an unchecked checkbox to mark your disagreement, and that concerns were to be used to file specific, discuss-able issues about the feature and its functionality.
I could have filed similar "I don't like this" concerns on other features I personally disagreed with (see e.g. uniform_paths), but I don't believe that infinite-filibuster is a productive way to do language design.
If there are specific concerns about the way that this feature interacts with other parts of the language that should be discussed / resolved (e.g. "this seems like it could be obviated by try { ... }
" or "it helps to enable un-idiomatically-large functions") I think it would be more productive to file them that way.
@cramertj I would not have filed such a concern in that form if @Centril hadn't explicitly suggested it. (I personally would have preferred if FCP had not been proposed.)
What would you suggest as the appropriate process for "this should be closed", if someone has filed a P-FCP with rfcbot for something other than "close"? "make sure that they are addressed / resolved appropriately" sounds equivalent to "this is going to be stabilized one day, what does it take to get there?". That doesn't leave a path in the flow for "this should never be stabilized, this should not be part of the language".
My understanding of the process was that the intent was to use an unchecked checkbox to mark your disagreement
Then we'd need to change the process back to requiring all checkboxes checked to proceed.
I don't believe that infinite-filibuster is a productive way to do language design.
I don't believe it is either. I'd like to find a path to conclude this, too, I just would like to see it concluded in a different direction.
For the record, based on experiences with this RFC, I don't think it's a good idea again to respond to an RFC with a caveat of "we can evaluate/address during stabilization whether we should proceed", because it seems clear to me that doing so produces critical procedural issues like this.
We need a path for saying "yes, I can see how this individual feature would make the language more expressive, but based on overall evaluation of the language, it doesn't carry its weight, and I don't want to further expand the surface area of the language in this way". I believe we need to regularly make that call, lest every proposed feature be seen as inevitable in some form and just a matter of quelling objections and persisting.
I will repeat what I've said before because it seems to have been missed: https://github.com/rust-lang/rust/issues/48594#issuecomment-451795597
I basically talked about the differences between using a loop and using a breaking block.
mainly, breaking blocks have different syntax, (slightly) different semantics (with respect to unlabeled break at the very least), they signify different intent, and a few other minor things.
does rfcbot support anticoncerns?
@joshtriplett
We need a path for saying "yes, I can see how this individual feature would make the language more expressive, but based on overall evaluation of the language, it doesn't carry its weight, and I don't want to further expand the surface area of the language in this way". I believe we need to regularly make that call, lest every proposed feature be seen as inevitable in some form and just a matter of quelling objections and persisting.
Yeah, it's an unfortunate hazard of our current process that either accepting or rejecting a feature require full consensus of the appropriate team. When the lang team is as large as it is now, it's difficult to always achieve this-- I thought from the comments above that this was just missing code examples to motivate why this feature is important, but it now sounds like you agree that there is code that can be better written using this feature, but aren't convinced that it is worth the costs you see here. I'm not sure of a way to convince you that these cases are sufficient motivation, as it seems like continuing to provide examples (including the macro example, which is literally impossible to write in another style) isn't enough.
Similarly, I'm fairly confident that I personally will remain convinced that this feature should be merged: IMO it not only pulls its weight through a variety of examples where it is the best/only option, but the language is actually simpler with its addition (since not allowing labelled blocks is surprising to me given that we allow other labeled constructs).
If you agree with my above summary of your position, then it seems we're at an unfortunate (and, I believe, historic!) impass lacking a process. One option is to intepret the current rfcbot rules as I did above to mean "no checkbox signals your disagreement, three members are necessary to overrule the majority", but I don't think that's how the rules were meant when they were introduced, so I think it was disingenuous of me to suggest that you should follow this procedure (though I did it myself elsewhere).
I've previously heard suggestions that we could introduce a time limit for features going from approved->implemented->stabilized, and that we should "auto-close" features which fall behind in an effort to avoid an ever-increasing backlog. Something like that could address this case, where I (and, I think, several others on the team) will not mark checkboxes to close, nor will you mark a checkbox to accept (I still even now feel afraid even saying this that I'm throwing away a last-ditch effort to convince you! :smile:).
I worry that with ever-growing teams we'll lose the ability to come to consensus on features, and that it will be hard to steer the language in a coherent fashion, particularly on the lang-design team. Team size limits seems like an obvious solution to this-- it's possible to receive input from a large number of people, but quite impossible to build absolute consensus among a sufficiently large group. Others will probably argue that contentious features shouldn't be merged in an effort to safeguard the language from misfeatures. I personally think the community is unlikely to let us make to many of those mistakes without adequate warning, but it's a thought.
I'll try and start a separate thread to discuss the process evolution here, and in the meantime I'd ask that folks chiming in on this thread please only post if there are new, critical use-cases to consider that are notably different from those above, or if there is a significant unconsidered reason why this feature should not be added to the language. Reading through the whole history on these megathreads is difficult, but it becomes even more-so when posts are repeated over-and-over, or when the thread is filled with unhelpful commentary. (he says having now typed one of the longest comment sin the whole thread XD)
TL;DR: I think we're stuck, we should have a process for this-- I'll start that conversation elsewhere and link it here. Otherwise, please don't comment unless you have significant new information that needs to be considered.
@cramertj
it's an unfortunate hazard of our current process that either accepting or rejecting a feature require full consensus of the appropriate team
I would honestly consider that a feature.
I thought from the comments above that this was just missing code examples to motivate why this feature is important, but it now sounds like you agree that there is code that can be better written using this feature, but aren't convinced that it is worth the costs you see here.
I still feel that many of the examples could be written in other ways. This does not add any unrepresentable expressiveness to the language. I originally felt that it could be motivated with sufficient examples, but the more examples I see that could use this feature, the more I find myself agreeing with @withoutboats that this feature simply shouldn't go into the language.
(Also worth noting: @nrc's crate proc-macro-rules
that used label_break_value
seems to have been rewritten to avoid it.)
One option is to intepret the current rfcbot rules as I did above to mean "no checkbox signals your disagreement, three members are necessary to overrule the majority", but I don't think that's how the rules were meant when they were introduced, so I think it was disingenuous of me to suggest that you should follow this procedure (though I did it myself elsewhere).
I do think that's the correct procedure for "abstaining", but not for objecting.
IMO it not only pulls its weight through a variety of examples where it is the best/only option, but the language is actually simpler with its addition (since not allowing labelled blocks is surprising to me given that we allow other labeled constructs).
I do typically find orthogonality arguments compelling, but this one in particular I find uncompelling, as personally I would have preferred not to have labeled loop breaks in the language either.
I worry that with ever-growing teams we'll lose the ability to come to consensus on features, and that it will be hard to steer the language in a coherent fashion, particularly on the lang-design team.
I worry not just about steering but about stopping. There's a certain inevitability that crops up sometimes, where the process seems focused on finding a path to "yes" and there's no graph edge in the flowchart that leads to "no", just "not yet".
There have been many posts written in 2018 and 2019 talking about feature growth in the language, and I feel that in the language team we need to give serious consideration to the total surface area of the language. And I feel like we don't have good processes that encourage us to do that.
@joshtriplett
This does not add any unrepresentable expressiveness to the language.
To be very clear: it does do exactly this. There is currently no way to express this code that does not interfere with other control-flow constructs, which (as others have pointed out) is especially desirable in macros, where this would be the only construct which is untargetable via un-labeled breaks, allowing macro-authors to break from sections without risking an overlap with a user-provided break
.
it also makes code more readable as you don't have to zig-zag around (if we had no labels at all - see Lua example) or do weird stuff with loops. this also has a small performance benefit even tho llvm should be able to optimize zigzag code.
@joshtriplett
I still feel that many of the examples _could_ be written in other ways. This does not add any unrepresentable expressiveness to the language. I originally felt that it could be motivated with sufficient examples, but the more examples I see that _could_ use this feature, the more I find myself agreeing with @withoutboats that this feature simply shouldn't go into the language.
Is that something we could dig into perhaps?
I do typically find orthogonality arguments compelling, but this one in particular I find uncompelling, as personally I would have preferred not to have labeled loop breaks in the language either.
This line of reasoning I find strange (unless you wish to remove labeled loop breaks with an edition). It does not seem appropriate to base design decisions based on what you wished weren't in the language. It is there, and so we should consider whether this addition is coherent with that. Otherwise there are many things I might have done differently about Rust, but I should not and cannot.
I worry not just about steering but about _stopping_. There's a certain inevitability that crops up sometimes, where the process seems focused on finding a path to "yes" and there's no graph edge in the flowchart that leads to "no", just "not yet".
Not yet is its own form of "no" in the sense that it won't get stabilized without a yes. Furthermore, there is a no: convince the rest of us that LBV is a bad idea / not sufficiently motivated for X, Y, and Z reasons. I have yet to hear many such concrete arguments at all. You have also demonstrated clearly here that there is no inevitability.
There have been many posts written in 2018 and 2019 talking about feature growth in the language, and I feel that in the language team we need to give _serious_ consideration to the total surface area of the language. And I feel like we don't have good processes that encourage us to do that.
I personally feel that many of these posts are either about sustainability (but not as aptly worded as @nikomatsakis did...) or that many folks don't understand how the language team operates (i.e. we already do give serious consideration to the total surface area). The total surface syntax of Rust has probably actually shrunk over the last year, not grown. LBV doesn't increase that notably and an argument could be made that it actually shrinks the number of productions in the language and makes the syntax more uniform.
Merely combining grammar productions does not shrink the surface area of the language.
Reducing the number of conditional branches one can take is arguably shrinking the language's surface area.
I don't think this particular feature goes either way, as such it's probably neutral. But e.g. things that unify language constructs (bool let, anyone?) do shrink surface area.
For what it's worth, Common Lisp, which is similar to Rust in the sense that it's a multi-paradigm language with macros, has this feature (named block
/ return-from
). Similar to what has been discussed here, in Common Lisp this construct is often helpful when writing macros and in expressing irreducibly complex control flow.
(block foo
(return-from foo "value"))
My sense is that in Common Lisp this feature is considered successful. It doesn't come up in conversations about features that make the language difficult to learn or implement.
There is the inclusion
early return from block ⊂ exceptions ⊂ call/cc
For example, in Scheme the following emulation of return-from
is feasible:
(define call/cc call-with-current-continuation)
(define-syntax block
(syntax-rules ()
((_ label statements ...)
(call/cc (lambda (label) (begin statements ...))))))
(block foo
(display "Visible text")
(foo "value")
(display "Phantom text"))
In Scheme, call/cc
is seen as successful as controversial. I find this particularly interesting because you first have to formulate an object in order to be able to talk about it. Even if you consider call/cc
as a misfeature, it made the language more substantial in an intellectual sense.
Nemerle language has Named blocks functionality.
It is very useful and convenient.
return, break and continue are implemented as macros using this feature.
@jhpratt Please read the discussion in the issue.
Repeating some comments I made on IRLO at @Centril's suggestion:
In discussing this feature, I suggested the following inspiration (counter to the usual loop-related version):
Loops aren't really my inspiration here, anyway. When I look at try blocks, I think "man it would be useful to be able to write some short local computation with early returns without having to go to the trouble of creating a closure". try blocks work for the rather constrained "I want to return an impl Try", and don't support any sort of labels.
The above is my primary use-case for this: I like early-return style. try {}
blocks get us most of the way there, but, again, they don't really support labels (not that you need to, because you'll write try {}?
for nested blocks) and make you shoehorn your type into a certain monadic shape that is not a catchall (as evidenced by the provided use-cases).
I also made some observations about C++, where not having label break/continue is very painful, especially in absence of Rust-like iterators. For example, there is no way to continue an outer loop from an inner loop without a possibly-UB goto
or a local break
plus a conditional continue
outside the inner loop.
At least C++'s lack of a borrow-checker makes the inline lambda trick less painful, which does, effectively, support LVB, since returning in the inline lambda gets turned into something like LVB by LLVM's inliner. Something like this is... quite a bit more questionable in Rust.
I should also point out that this feature is approximately equivalent in expressiveness to Go's goto
, which compile-time enforces not jumping over declarations and whatnot (frankly, if Rob Pike thought goto
was acceptable, given the history of trying to bat down C++'s problems, I'd trust him on that one somewhat).
Also, if we're going to go into prior art, Kotlin also provides this exact feature, in the form of return@label expr;
.
In c'ish languages I often use labels, even without any incoming goto, as locations for stable breakpoints under loops, because the debugger can recognize break function:label
.
Even without consensus on break, labels might be nice.
Edit: One potential hurdle, is that typically labels follow symbol naming convention, if I understand the RFC, these labels do not follow symbol naming convention where ', isn't valid in a symbol name.
I'm not sure offhand in e.g. Dwarf, or gdb itself if there is in fact any issue here though.
Edit2:
Pretty sure there is some smoke to this, if we look at the quoting behavior of normal c based labels,
in the debugger, gdb at least is going to treat the quotes, for quoting rather than part of the symbol name. The following results in
Breakpoint 1 at 0x401114: file
, line 1.
unmatched quote
echo "void main() { } void hmm() { umm: return; }" | gcc -g -x c -;
gdb -ex "b hmm:'umm'" -ex "b hmm:'umm" -batch ./a.out
And I don't believe this is able to be affected by the rust-language language specific support in gdb, as I believe this quoting happens before symbol matching.
Edit: The ship has probably sailed on this due to existing loop labels.
A minor point in favor of early return from blocks would be poor man's contract programming. One simply adds assert statements as pre- and postconditions. In order to maintain comfort, it should be possible to replace return
with break 'ret
to allow this construct:
let value = 'ret: {
// ...
};
assert!(postcondition(value));
return value;
This is an imperfect solution, however, because return
should be forbidden inside of the block.
Adding a note that I've wanted this feature but did not use it because I didn't know it existed, which I feel is a negative modifier on "people don't want it as evidenced by how few people use it".
I independently reinvented the concept on IRLO this morning.
I've been playing around with this today, and it comes in _super_ handy in async
blocks. However, it seems to run into issues when combined with the try_blocks
feature:
#![feature(try_blocks, label_break_value)]
fn main() {
let _: Result<(), ()> = try {
'foo: {
Err(())?;
break 'foo;
}
};
}
error[E0695]: unlabeled `break` inside of a labeled block
--> src/main.rs:6:20
|
6 | Err(())?;
| ^ `break` statements that would diverge to or through a labeled block need to bear a label
error: aborting due to previous error
try blocks are a mistake.
... can't you label the ?
itself? (as in Err(()) 'foo?;
)
I strongly disagree that try blocks are a mistake, though that is a separate discussion, and probably not worth going back and forth on here.
In this particular example it might be doable, but this is very minimized compared to the real code I have, where 'foo
contains a decent chunk of code, and several ?
s.
@SoniEx2
try blocks are a mistake.
This comment is inappropriate. @jonhoo's comment was reporting a (presumably) buggy interaction. Regardless of one's opinions about try
blocks (or label-break-value) it's clear that they should interoperate smoothly.
they should, with the Err(()) 'foo?;
syntax.
@jonhoo I suspect you're seeing impl details leaking out in terms of how try
is desugared -- can you file that as a separate issue and we can move the discussion of possible fixes there?
The RFC says that
'BLOCK_LABEL: { EXPR }
is syntactic sugar for
'BLOCK_LABEL: loop { break { EXPR } }
I tried making that substitution, and the code compiles, with a warning about unreachable code.
#![feature(try_blocks, label_break_value)]
fn main() {
let _: Result<(), ()> = try {
'foo: loop {
break {
Err(())?;
break 'foo;
}
}
};
}
@nikomatsakis @ciphergoth filed as https://github.com/rust-lang/rust/issues/72483.
I find that I no longer object to this. I would have objected more strongly to the initial concept of labeled break if it were up for consideration today, but given that that concept exists, I don't think it makes sense for me to continue objecting to its application to arbitrary blocks.
(This applies to the current form, using break
, not to any other syntax.)
@rfcbot resolve should-close-not-merge
@rfcbot reviewed
@joshtriplett For what it's worth, I have found this to be immensely useful in async
blocks, since it's the only way to do an "early return". It means that instead of writing:
async {
// do thing a
if thing_a_failed {
// handle specially (note, _not_ ?)
} else {
// do thing b
if thing_b_failed {
// handle specially (note, _not_ ?)
} else {
// do thing c, etc..
}
}
}
I can write:
async {
'block {
// do thing a
if thing_a_failed {
// handle specially (note, _not_ ?)
break 'block;
}
// do thing b
if thing_b_failed {
// handle specially (note, _not_ ?)
break 'block;
}
// do thing c, etc..
}
}
This is neatly analogous to how you can early-return with return
in functions/closures, and with continue/break
in loops. Admittedly it'd be nice if I didn't need the extra block (async 'block {
a possibility?), but it definitely beats the nested if-s.
Allowing async blocks to be directly annotated with labels sounds like a very good extension to this feature.
@rfcbot fcp cancel
I'm going to cancel the FCP here as it's been blocked forever. We should probably discuss whether we want to push this future through. If nothing else, it seems like it should be updated to take async blocks into account, and it sounds like that adds a new use case for the feature.
@nikomatsakis proposal cancelled.
Note that there is no ambiguity about the semantics of this proposal in the presence of async blocks: the definition in the RFC still applies ie
'BLOCK_LABEL: { EXPR }
is simply syntactic sugar for
'BLOCK_LABEL: loop { break { EXPR } }
except that unlabelled breaks or continues which would bind to the implicit loop are forbidden inside the EXPR.
Note that you can (early) return
from async blocks, instead of labelled break
, so labelling async blocks doesn't make much sense:
let fut = async {
return 42;
0
};
println!("{}", fut.await); // prints 42
@WaffleLapkin I actually just came over here to note that as I was recently informed of that myself! I do think the feature is still very useful for being able to _skip_ sections of code (don't run the rest of this block, but also don't return), but its applicability for async
_specifically_ is less than I initially thought.
Most helpful comment
@withoutboats
I agree with this principle, and I think this feature meets that bar (familiar control flow targeted at specific, important use-cases). There are a number of times when I've reviewed or written code like the examples provided above where I feel that this is by far the cleanest and easiest-to-read way to write this code. Reusing existing control-flow constructs like
loop
with an unconditionalbreak
at the end misleads the user, and immediately-applied functions are often insufficient due to use of?
,await!
, or other intermediate control-flow constructs.Using
loop
+ trailingbreak
is confusing, and we should prefer instead to have users state their real intention over requiring them to abuse a tool that was made for a different style of control-flow. This is similar to the fact that we even have aloop
construct, while other languages are content withwhile true { ... }
. Doing this makes it possible to write code that expresses a more clear intent and is more readable as a result.In addition, this feature is something that I've always expected Rust to have, given that we have labeled-most-other-things and break-from-labeled-things, the fact that you cannot label or break a block seems confusing and wrong to me.
TL;DR: I think this feature is supporting real-world use-cases that can only be written otherwise through heavily-nested-
if
statements or by abusing other constructs like loop-break-value. It's a small addition to the surface syntax that makes blocks behave as I would expect them to behave and allows me to write the code I mean rather than something much hackier.