-
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
Allow loops to return values other than () #961
Comments
I'm just starting with rust, but why would loops not just return their last statement like everything else seems to do, functions,
The |
@JelteF the problem is that a loop without a return or break statement will never return, for that reason you cannot end your loop on an expression. |
@ticki |
@withoutboats Yeah, that's right. |
You want to make loops return value of last expression? You want to make it implicit without any break?
I think this is ugly. |
Update: never mind the note below (which is preserved just so the responses that follow continue to make sense).
@JelteF another reason to not just use the last expression in the loop-form's body as its return value is that it would be a breaking change: loop-form bodies today are allowed to end with an expression that is evaluated and then discarded. Returning the last expression implicitly would change the dynamic extent of the returned value, which in turn would change the lifetime associated with it and its r-value temporaries. And that change would probably inject borrowck errors into existing stable code. (The alternative of solely using |
@ticki You might have misunderstood. I only said we did not need the @KalitaAlexey I did suggest code like that, but the break could still be used. It is a very good point that you are making though. I had not thought about the case that the loop would never be evaluated. It seems you are right that the @pnkfelix I'm not sure what the breaking change is, since the check for the type of the return value could simply be skipped in cases where it is not saved in a value. |
Please, no. Python has this, and every time I encountered it I had to jump into REPL and see when this I’m not that opposed to being able to “return” something from the loop with a |
I think that an else block is strictly more expressive than returning the final expression and only thing that makes sense in the presence of value-returning breaks. If the value of final expression is going to be returned, why should the evaluation of the last run of the loop be any different from any other run? And are the final expressions (given that there is no side effects) evaluated for nothing on all the other runs? Optimizing them off becomes then burden on the compiler. If the value-returning break isn't hit, then there needs to be an alternative path that returns a value of the same type. It doesn't have to be named "else", but I think that's a sensible name. |
This bites me every time too, mainly because while useful its a rarely used feature. Maybe a better/more accurate name would help? (Nothing immediately springs to mind though.) Or perhaps something slightly different, like having a for x in iterator {
if foo(x) {
break "yes!";
}
} default {
"awww :("
} Where the default expression is evaluated either if |
I agree that the I think another name would indeed be good. Something that comes to my mind would simply be PS. I retract my initial proposal about using the last statement instead of the |
FWIW I think the best way to move forward on this, incrementally, would be to start by only allowing |
@glaebhoerl |
This can kind-of be already done as {
let _RET;
for x in iter {
if pred(x) {
_RET = x;
break;
}
}
_RET
} |
Yet again, @glaebhoerl says exactly what I was going to say :). Somebody want to make an RFC for this, I'd be willing to help? @JelteF heh, that's kinda the point! Once people see how nice this is, there will be more motivation to actually reach a consensus on break-with-value for other types of loops (and maybe even normal blocks!). |
@arielb1 Of course it can be done, but the point is that this: let a = for x in 1..4 {
if x == 2 {
break x
}
} nobreak {
0
} looks much cleaner than this: let a = {
let mut _ret = 0;
for x in 1..4 {
if x == 2 {
_ret = x;
break;
}
}
_ret
} @Ericson2314 It seems that if the only consensus that needs to be reached is the naming, it could be solved rather quickly. It would be weird to hurry an incomplete proposal, if all that needs to be done is pick a name for a statement. |
@JelteF Well I'll grant you that originally there were more ideas, but because #955 did not happen <some loop> {
...
break; // as opposed to break ();
...
} else {
my_fun() // returns ()
}; @nagisa Anyone playing around will notice that the type checker will require |
@Ericson2314 I don't see a reason why that should not work. In Python it is not an expression and it still has a use. Namely handeling the edge case when the loop does not break. A simple example can be found here: https://shahriar.svbtle.com/pythons-else-clause-in-loops#but-why for x in data:
if meets_condition(x):
break
else:
# raise error or do additional processing vs condition_is_met = False
for x in data:
if meets_condition(x):
condition_is_met = True
if not condition_is_met:
# raise error or do additional processing As for your comment @nagisa. In this case it might not be directly clear what the |
I passionately hate the idea itself of having an I don’t want to see any of that weirdness in Rust just because Python has it. One might argue for a new keyword, but that’s ain’t happening either, because of backwards compatibility. EDIT: All looping constructs have trivial desugarings into a plain old |
@nagisa Sure, but the desugarings of |
@nagisa |
@nagisa Personally, it reminds me of the base case of a fold, and thus actually feels quite elegant. |
@nagisa If all looping constructs desugar into I just came to think of another possibility for
This is nice in the sense that it doesn't need any new keywords or reusing old keywords in surprising way. |
@golddranks But that's confusing. It is like functional programming but ugly. |
Another note: sometimes I've written a loop that is expected to set some outer state inside the loop. But because setting the state (in that particulal case was) may be expensive, you might want to avoid setting a "default" state before running the loop. But this results in the fact that the control flow analysis can't be sure if the state is set in the end, since it's possible that the loop runs 0 times. I have to make a boolean flag to check, and even then, if the analysis isn't super smart, it won't be sure. Having a |
Per #1767 I'm closing this issue. We now support We're open to revisiting this some day if conditions change a lot. For example, possible now that |
In case this ever gets revisited, how about combining @glaebhoerl's idea of moving the break into the block and using the 'final' keyword as proposed by @canndrew:
I would find the meaning obvious enough reading this code even if I was not familiar with the feature (something I can't say about python's for/else). |
I would suggest
|
For people not having enough time to read through the whole set of comments here, summary:
I wonder if the backward-compatibility issues are no longer such an issue now that we have the "edition" feature and the associated |
I'll suggest |
Hello, from Rust newcomer in 2021 who interested in compiler development. I like this one, and I think it could be done without introducing a new keyword, being parsed with one lookahead, using for (...) {}
// if current is `!` and next is `break` then parse noReturn clause We've got this: let a = for x in 1..4 {
if x == 2 {
break x
}
} !break {
0
} This won't break the parser as I see, as far as UPD: Sorry, didn't see that this is already proposed 😝 |
'finally" and "then" both sound like it's always executed. |
Else does work well in a lot of cases but can be a little confusing in others. I have definitely needed to explain it to people before in python. As you said it works well for Overall even if not "perfect" I think it is the best option suggested so far by a fair margin. |
What do people really mean when they say that It is definitely unfamiliar to a lot of programmers and one can’t very easily guess what it does, but I don’t believe it is actively misleading either. It’s not like it exists elsewhere with a different meaning, or one could easily guess incorrectly. When someone encounters it for the first time they will not know what it means, and can easily look it up. In a search engine the first few results for "for else python" all do a decent job at explaining it. |
I think a significant problem is that it can be interpreted as different
things for example "If the loop runs at all, else". The meaning being
proposed here "If the loop breaks, else" is not particularly obvious. So
yes, unfamiliar. But also unintuitive. You probably would have a hard time
guessing if you didn't look it up. There is a good chance you could figure
it out from context, but the meaning probably wouldn't be the first thing
that jumps out at you.
|
The "If the loop runs at all, else" interpretation is interesting also in the sense that the loop body can have side effects. In case some of those side-effects are things that matter from the viewpoint of control flow, such as "does a variable get initialized", the else body becomes a language-level enabler for being able to allow initializing variables in the case the loop body didn't initialize them. It's a pattern that the compiler ought be able to reason about, unlike the impossible task of reasoning about the behaviour of arbitrary iterators. So, I'd say there are two interesting "else" cases with loops:
The names "break-or-else" and "run-or-else" are just for clarification, I don't have any concrete syntax proposals, but I think that if there would be a clear, understandable syntax for both, they both would be a valuable additions in an expressive language like Rust. |
The “if the loop runs at all, else” interpretation is not possible, because this would not result in a value: let a = for x in 1..4 {
if false {
break x;
}
} else {
0
}; |
@andersk that's why I'm saying that a for loop with a "run-or-else" block but no "break-or-else" block should behave like the current for and not evaluate to anything (other than ()). To elaborate further, I'm not arguing that the for loops should have else blocks, or if they would, what their semantics should be; I'm arguing that there're two interesting cases with for loops that warrant running a conditional body of code. Actually, I think that's an argument for that loops shouldn't have else blocks at all, since it's not clear, which sensible semantic they have. A possible syntax just occured to me: |
If you're willing to add new keywords, there's any number of possibilities that are clearer than This idea of |
Indeed, by itself it's offtopic, but existence of another sensible semantic for |
I assume you're talking about variable assignments/bindings:
|
@dhardy I've been following this thread for years, and I just noticed that I'm running in loops myself ( The same take, earlier: #961 (comment) ). I just want point out that I feel your argument about variable assignment actually convincing, so thank you; I wish I'd remember what I originally felt the need for. The abstract argument is clear: to be able to guarantee a side-effect at least once, but the concrete need; not so much anymore. I still prefer |
It could work with letting the body of the loop be non-unit. For example, let last =
for x in whatever {
Some(x)
} else {
None
}; |
let a = for x in 1..4 {
if false {
break x;
}
} else {
0
}; Here's a CFG branch that does not point to any target value (here it is code after
The 1. option is simple makes introducing I am upset by the fact that the so nice feature has no good way to implement. Also, if the value of let a = for x in 1..4 {
} else {
0
}; Becomes let a = loop {
// Iterator lowering stuff without running
// This is the first run of iterator next
match iterator.next() {
Some(T) => // Continue iterating keeping in mind that one iteration already done
None => // Run `else` clause
}
}; By doing so, all |
(slightly offtopic but important to consider) In the future when generators are stabilized it would make sense to allow iterating over yielded values with the for loop and the whole loop could evaluate to the return type of the generator. So if you have for i in gen {
// do something with i
}?; // handle error |
@phaux I went over this. (Not just once, but on Internals before as well.) |
Extend for, loop, and while loops to allow them to return values other than ():
Proposed in #352
Some discussion of future-proofing is available in #955
The text was updated successfully, but these errors were encountered: