[Language] for-else and while-else
when binding to a bindable, break in for and while isn't allowed, this proposal allow it using a else
-
when
breakis not reached, evaluates to value inelse -
semantic similar to Zig
https://ziglang.org/documentation/master/#for
read the comment:
// For loops can also be used as expressions.
// Similar to while loops, when you break from a for loop, the else branch is not evaluated.
- semantic different of Python (
breakand the proposedelseblock outputs an expression in Rust, in Python you can't do it)
Example:
fn main() {
let result = for counter in 0..5 {
if counter == 3 {
break counter
}
} else { -7 };
println!("The result is {result}") // 3
}
fn main() {
let result = for counter in 0..5 {
if counter == 90 {
break counter
}
} else { -7 };
println!("The result is {result}") // -7
}
Alternatives
https://rust-lang.github.io/rfcs/2046-label-break-value.html
Alternatives to else keyword
IDK
This was previously suggested in #3163, which was closed due to concerns over the clarity of the syntax.
This was previously suggested in #3163, which was closed due to concerns over the clarity of the syntax.
when break is not reached, evaluates to value in else, what's the struggle?
@cindRoberta the behavior in most other languages only runs else if no iterations happened at all, no matter break or not
@cindRoberta the behavior in most other languages only runs
elseif no iterations happened at all, no matterbreakor not
@SOF3 in other languages, for and while aren't expressions. It's a weird behaviour for and while ever return (), the behaviour that I proposed is the same of Zig, Zig uses else
I proposed a different behaviour than Python
https://ziglang.org/documentation/master/#for
read the comment:
// For loops can also be used as expressions.
// Similar to while loops, when you break from a for loop, the else branch is not evaluated.
I'm not creating nothing, it already exists
Yes, but that's exactly the reason why it is extremely confusing if someone ends up using it as a non-expression. This is just as confusing as case without break not falling through in golang because everyone is used to that fact.
Yes, but that's exactly the reason why it is extremely confusing if someone ends up using it as a non-expression. This is just as confusing as
casewithoutbreaknot falling through in golang because everyone is used to that fact.
if, for, while and loop in Rust are ever evaluating for a value, same when not binded to a variable. Just that for and while are ever evaluating to () and if and loop not
if the struggle is the confusion with Python, use other keyword than else
But what you can do with for-else can already be done with iterators right? break-else is basically the same as iter.find_map(...).unwrap_or_else(...).
But what you can do with for-else can already be done with iterators right? break-else is basically the same as
iter.find_map(...).unwrap_or_else(...).
@safinaskar what can to be done with for-else can already be done too with labels, for-else is just fill a gap of ever evaluate to ()
@cindRoberta , what you want is already possible thanks to recently stabilized "break from labeled blocks" ( see https://blog.rust-lang.org/2022/11/03/Rust-1.65.0.html ). Here is how we can write your example with recent stable Rust: https://play.rust-lang.org/?version=stable&mode=debug&edition=2021&gist=4981da6fe916fc970922ebcd8e639e2c
@cindRoberta , what you want is already possible thanks to recently stabilized "break from labeled blocks" ( see https://blog.rust-lang.org/2022/11/03/Rust-1.65.0.html ). Here is how we can write your example with recent stable Rust: https://play.rust-lang.org/?version=stable&mode=debug&edition=2021&gist=4981da6fe916fc970922ebcd8e639e2c
https://rust-lang.github.io/rfcs/2046-label-break-value.html
Here's are the relevant links I've found:
Previously proposed in:
- https://github.com/rust-lang/rfcs/pull/352
- https://github.com/rust-lang/rfcs/issues/961
- https://github.com/rust-lang/rfcs/issues/1767
- https://github.com/rust-lang/rfcs/issues/3152
- https://github.com/rust-lang/rfcs/pull/3163
- https://www.reddit.com/r/rust/comments/n7l028/for_else/
- https://internals.rust-lang.org/t/allow-loops-to-return-values-other-than/567
- https://internals.rust-lang.org/t/pre-rfc-break-with-value-in-for-while-loops/11208/
- https://github.com/ftxqxd/rfcs/blob/loop-else/text/0000-loops-returning-values.md
- https://github.com/fstirlitz/rust-rfcs/blob/exhaustive_ballot/text/0000-loop-exhaustion.md
- https://github.com/rust-lang/rfcs/blob/master/text/1624-loop-break-value.md#extension-to-for-while-while-let
Alternative features and proposals:
- https://programming-idioms.org/idiom/223/for-else-loop/3852/rust
- https://rust-lang.github.io/rfcs/2046-label-break-value.html, https://github.com/rust-lang/rfcs/blob/798b894319c4b79e168dd160678166456be633c6/text/2046-label-break-value.md, https://github.com/rust-lang/rfcs/pull/2046, https://github.com/rust-lang/rust/issues/48594
- https://doc.rust-lang.org/reference/expressions/loop-expr.html#break-and-loop-values
- https://internals.rust-lang.org/t/pre-rfc-generator-integration-with-for-loops/6625
- https://internals.rust-lang.org/t/break-with-value-alternatives/11240
- https://doc.rust-lang.org/std/iter/trait.Iterator.html#method.try_find, https://github.com/rust-lang/rust/issues/63178, https://github.com/rust-lang/rust/pull/63177
Similar features or proposals in other languages with the semantics you're proposing:
- Python: https://docs.python.org/3/tutorial/controlflow.html#break-and-continue-statements-and-else-clauses-on-loops, https://docs.python.org/3.4/reference/compound_stmts.html#for, https://docs.python.org/3.4/reference/compound_stmts.html#the-while-statement, https://s16h.medium.com/the-forgotten-optional-else-in-python-loops-90d9c465c830 (but see also https://blog.glyphobet.net/blurb/2187/, which includes https://twitpic.com/4a52sh)
- Zig: https://ziglang.org/documentation/master/, search for
test_while_else.zigandtest "for else" - Go: https://github.com/golang/go/issues/41348, https://github.com/golang/go/issues/24282
- Julia: https://github.com/JuliaLang/julia/issues/1289#issuecomment-48820514, https://github.com/JuliaLang/julia/issues/22891, https://github.com/JuliaLang/julia/pull/23260
Features or proposals with similar syntax but semantics to run the else if the loop never iterates:
- PHP: https://wiki.php.net/rfc/loop_else, https://bugs.php.net/bug.php?id=26411, https://bugs.php.net/bug.php?id=46240, https://bugs.php.net/bug.php?id=61222
- Jinja (Python template engine): https://jinja.palletsprojects.com/en/3.0.x/templates/#for
- Julia: https://github.com/JuliaLang/julia/issues/1289
- Twig (PHP template engine): https://twig.symfony.com/doc/2.x/tags/for.html#the-else-clause/
For-else (with Python semantics) implemented as a macro: https://github.com/erkinalp/pythonic-for
For-else (with Python semantics) implemented as a macro: https://github.com/erkinalp/pythonic-for
I have updated this proposal, please, read it again
Isn't this proposal the same as Python's semantics, except that the for loop is an expression rather than a statement, and so can have a value? If it's different beyond that, what is the difference?
https://docs.python.org/3/tutorial/controlflow.html#break-and-continue-statements-and-else-clauses-on-loops:
In a
fororwhileloop thebreakstatement may be paired with anelseclause. If the loop finishes without executing thebreak, theelseclause executes.In a
forloop, theelseclause is executed after the loop finishes its final iteration, that is, if no break occurred.[...]
In either kind of loop, the
elseclause is not executed if the loop was terminated by abreak. Of course, other ways of ending the loop early, such as areturnor a raised exception, will also skip execution of theelseclause.
(formatting copied from source)
Here's an approximate port of your example to Python, which gives the same result: Try it online!
for stop_condition in [3, 90]:
print("stop_condition =", stop_condition)
for counter in range(5):
if counter == stop_condition:
result = counter
print("breaking at", counter)
break
else:
result = -7
print("in else clause")
print("The result is", result)
print()
Output:
stop_condition = 3
breaking at 3
The result is 3
stop_condition = 90
in else clause
The result is -7
with label-break-value this is supported as:
let result = 'result: {
for counter in 0..5 {
if counter == 3 {
break 'result counter;
}
}
-7
};
Pro: No need to explain how an else clause works
Con: One more indentation level
Alternatives to else keyword
IDK
I scowered through this discussion a few times but this hasn't been brought up...
The final kw is reserved without use!
We can have this be valid rust syntax:
let fancy = for x in xs {
if x.is_fancy() {
break Some(x);
}
} final {
None
}
Which would be equiv to:
let mut iter = xs.into_iter();
let fancy = loop {
let Some(x) = iter.next() else {
break None
};
if x.is_fancy() {
break Some(x)
}
}
I personally think this avoids the issue of the semantics not being clear on when the final block triggers, as its literally in the name final.
I think there's still some potential confusion with "finally" as in "try-catch-finally", but I don't think this would happen in practice as it wouldn't make sense for that to be the behaviour (you could place logic that happened unconditionally after the for loop... after the for loop).
Overall I personally think this is more then worth it for the clarity it could bring to codebases.
This behaviour could also by unified with normal for syntax by having a break without value return unit, the default behaviour could be Default::default() or just "left" as (). Hopefully this would make it easier to implement.
@sollyucko in fact the only difference to the Python for-else is that it's an expression and must output a value in break or in the else block. I put that it's like the Zig for-else because they are semantically more similar
Yes, but that's exactly the reason why it is extremely confusing if someone ends up using it as a non-expression. This is just as confusing as
casewithoutbreaknot falling through in golang because everyone is used to that fact.
@SOF3 is this confusing?
fn main() {
if true { 2 } else { 4 };
}
it's a valid Rust code
I understand emphasizing the similarity to Zig, but why distinguish it from Python, especially without specifying what the difference is? I guess the comment you quoted does hint at it, but it wasn't very obvious to me.
I was especially confused what you meant because I see the expression-vs-statement difference as being about allowed syntax (what constructs can be used in the language) rather than semantics (what those those constructs mean).
In other words: If you were to translate a Python for-else loop into Zig syntax, would it behave in the same way? If you were to translate a Zig for-else loop that doesn't output a value (or the value is ignored) into Python syntax, would it behave in the same way? If the answer is 'yes' to both of those, then I would say their for-else loops have the same semantics.
@sollyucko a Python for-else works in Zig, but a Zig for-else doesn't works in Python, it means they are semantically different
I have updated the proposal, please read it again
slightly updated my proof of concept: https://github.com/erkinalp/pythonic-for
@SOF3 is this confusing?
fn main() { if true { 2 } else { 4 }; } it's a valid Rust code
I'm not saying that a statement in other languages being an expression in Rust is confusing. I'm saying that a less-commonly-used feature in other languages with completely different semantics would be confusing if not astonishing. Someone may be using this as a () statement, which might lead to completely incorrect behavior.
for n in iter {
do_something();
} else {
iter_is_empty();
}
which would end up running iter_is_empty() every time. (Of course, this can mostly be prevented by adding a warn-by-default lint when for-else is used without any explicit break-with-value or if the output type is ())