rfcs icon indicating copy to clipboard operation
rfcs copied to clipboard

[Language] for-else and while-else

Open igotfr opened this issue 2 years ago • 25 comments

when binding to a bindable, break in for and while isn't allowed, this proposal allow it using a else

  • when break is not reached, evaluates to value in else

  • 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 (break and the proposed else block 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

igotfr avatar Dec 27 '22 01:12 igotfr

This was previously suggested in #3163, which was closed due to concerns over the clarity of the syntax.

PatchMixolydic avatar Dec 28 '22 18:12 PatchMixolydic

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?

igotfr avatar Dec 30 '22 17:12 igotfr

@cindRoberta the behavior in most other languages only runs else if no iterations happened at all, no matter break or not

SOF3 avatar Dec 31 '22 05:12 SOF3

@cindRoberta the behavior in most other languages only runs else if no iterations happened at all, no matter break or 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

igotfr avatar Dec 31 '22 17:12 igotfr

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.

SOF3 avatar Jan 01 '23 11:01 SOF3

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.

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

igotfr avatar Jan 03 '23 22:01 igotfr

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(...).

SOF3 avatar Jan 04 '23 03:01 SOF3

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 ()

igotfr avatar Jan 04 '23 19:01 igotfr

@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

safinaskar avatar Feb 09 '23 23:02 safinaskar

@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

igotfr avatar Feb 11 '23 01:02 igotfr

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.zig and test "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/

sollyucko avatar Sep 16 '23 17:09 sollyucko

For-else (with Python semantics) implemented as a macro: https://github.com/erkinalp/pythonic-for

erkinalp avatar May 16 '25 19:05 erkinalp

For-else (with Python semantics) implemented as a macro: https://github.com/erkinalp/pythonic-for

I have updated this proposal, please, read it again

igotfr avatar May 21 '25 19:05 igotfr

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 for or while loop the break statement may be paired with an else clause. If the loop finishes without executing the break, the else clause executes.

In a for loop, the else clause is executed after the loop finishes its final iteration, that is, if no break occurred.

[...]

In either kind of loop, the else clause is not executed if the loop was terminated by a break. Of course, other ways of ending the loop early, such as a return or a raised exception, will also skip execution of the else clause.

(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

sollyucko avatar May 22 '25 01:05 sollyucko

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

kennytm avatar May 22 '25 01:05 kennytm

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.

Vi-Kitten avatar Nov 09 '25 13:11 Vi-Kitten

@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

igotfr avatar Nov 18 '25 04:11 igotfr

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.

@SOF3 is this confusing?

fn main() {
  if true { 2 } else { 4 };
}

it's a valid Rust code

igotfr avatar Nov 18 '25 05:11 igotfr

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 avatar Nov 18 '25 05:11 sollyucko

@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

igotfr avatar Nov 18 '25 05:11 igotfr

slightly updated my proof of concept: https://github.com/erkinalp/pythonic-for

erkinalp avatar Nov 19 '25 08:11 erkinalp

@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 ())

SOF3 avatar Dec 08 '25 03:12 SOF3