Allow the infinite `loop` keyword to prefix expressions w/ blocks

Change the syntax of InfiniteLoopExpression from "loop BlockExpression" to "loop ExpressionWithBlock", excluding LoopExpression.

In addition, if the control flow of the inner expression is not exhaustive, a default case should be appended that breaks out of the loop automatically.

This would allow for the following constructs:

  • loop match / loop MatchExpression

    This would permit repeated partial match expressions that end the loop once they fail to match:

    loop match value {
        (Some(x), ..) | (_, Some(x), ..) | (_, _, Some(x)) => {
            sum += x;
        }
    }
    

    In general, this would be equivalent to:

    loop {
        match value {
            (Some(x), ..) | (_, Some(x), ..) | (_, _, Some(x)) => {
                sum += x;
            }
            _ => break
        }
    }
    

    If the match expression is exhaustive, a value can be returned using loop_break_value. If not exhaustive, the injected break prevents this behavior.

    Typically match is infallible, but a default break case is commonly useful when using match in a loop, such as to parse a value statefully. A similar existing syntax is while let, although such syntax can't be chained and only matches a single pattern.

  • loop if / loop IfExpression

    By default, this would be equivalent to a while statement.

    let mut x = 10;
    loop if x != 0 {
        println!("x: {x}");
        x -= 1;
    }
    

    Or in general:

    let mut x = 10;
    loop {
        if x != 0 {
            println!("x: {x}");
            x -= 1;
        } else {
            break
        }
    }
    

    However, this syntax additionally permits the inclusion of an exception case using the else keyword; consistent with loop match. The else case would be included in the loop, requiring a manual break if one is desired.

    let mut x = 10;
    loop if x != 0 {
        println!("x: {x}");
        x -= 1;
    } else {
        println!("end");
        break
    }
    

    This would be particularly useful for loop-if-else-if`-style expressions, similar to a match statement but allowing for different variables.

    let mut x = 10;
    let mut y = 20;
    loop if x != 0 {
        println!("x: {x}");
        x -= 1;
    } else if y != 0 {
        println!("y: {y}");
        y -= 1;
    }
    

    If the if-else expression is exhaustive, a value can be returned using loop_break_value. If not exhaustive, the injected break prevents this behavior.

    A potential downside is that the else case could be confused for a final exception case that runs after the loop ends. This likely shouldn't be the actual behavior as to allow chained else-if looping and remain consistent with loop match, but it could seem ambiguous.

  • loop if let / loop IfLetExpression

    Similar semantics to: loop IfExpression.

  • loop unsafe / loop UnsafeBlockExpression

    Equivalent to: loop { unsafe { .. } }. Just a convenience for consistency I suppose.

In addition, this syntax would help reduce excessive indentation; a.k.a. "rightward drift".

Related discussions:

Given that you've found those relevant previous discussions, have you any thoughts on the counterarguments that were raised within them?

4 Likes

Or that runs if the loop never runs -- other languages do have that construct, so the potential seems high to me.

5 Likes

All of the related discussions propose a loop match, but they all go overboard with entirely new disparate syntax (like for match x in iter or endless unbracketed keyword chaining). They mostly just debate that other syntax.

They also generally define loop match as syntax sugar for loop { match .. }, which people either seem okay with as a convenience, or dislike it for being unnecessary. However, I think the addition of the implicit break as a default case of fallible control flow is a big difference with my proposal that makes it hold more water as a unique utility.

I would almost want to make loop if {..} else .. invalid syntax, but I feel like having exceptions to the general rule is awkward or unintuitive in its own way. I'm unsure if there's a good rule for it that wouldn't seem ambiguous. Maybe just loop match on its own would be better.

This is my thought -- but about loop match and such too.

"Yes, it needs braces. If you have so many nested loops that the indentation is high, make a function" seems like an entirely reasonable answer to me. Otherwise there's a huge trail of "but I want else loop" and such where all the same motivations hold, and I'd rather just do none of them.

8 Likes

Yeah I think at that point it should just be a macro with the same functionality. The syntax loop x {} seems very natural and convenient, but it seems like a bad fit for Rust after thinking about it more. I'll leave the post up for posterity or any further discussion.

1 Like

Just loop match please. If we allow any "expression w/ block" after loop then loop loop loop { ... } would be legal.

What problem are you actually trying to solve? I don't see any problem here, the examples with the nested block are perfectly clear to read to me.

1 Like
let mut x = 10;
let mut y = 20;
loop if x != 0 {
    println!("x: {x}");
    x -= 1;
} else if y != 0 {
    println!("y: {y}");
    y -= 1;
}

such code is extremely dangerous, since you cannot figure that, the loop if is a infinite loop that forget a break, or a non-exaustive block that should adding an auto break. Your code could be

loop if x != 0 {
    println!("x: {x}");
    x -= 1;
} else {
    if y != 0 {
        println!("y: {y}");
        y -= 1;
    }
    break; // with accidently forget adding it.
}

I tried the current rust version, got

$ rustc --edition 2021 test2.rs && cat $_
warning: unreachable pattern
 --> test2.rs:6:9
  |
6 |         _    => println!{"wtf?"}
  |         ^
  |
  = note: `#[warn(unreachable_patterns)]` on by default

warning: 1 warning emitted

fn main(){
    let a:bool=true;
    match a {
        true => println!{"t"},
        false=> println!{"f"},
        _    => println!{"wtf?"}
    }
    if a {
        println!{"t again"}
    } else if !a {
        println!{"f again"}
    } else {
        println!{"wtf??"}
    }
}

It seems that, rust could detected unreachable match patterns, but cannot detect unreachable if-else patterns. Thus with your loop if statement, some overhead might be added accidently.

And for a most dangerous thing: How could you assume the missing branch is just a break rather than doing nothing? Suppose I have a data receiver rec, with a method receive(&'a mut self, timeout_ms:u32)->Option<&'a mut [u8]> Then I might write code like:

loop match rec.receive(1000) {
    Some(data)=>{if dealing_with(data).is_done() {break;}},
    None => () // waiting for more data
}

or just

loop if let Some(data) = rec.receive(1000) {
    if dealing_with(data).is_done() {
        break;
    }
}

In this case, how you could assume the else branch is break rather than continue?

I think I should've explained that part better in the post:

I find myself reaching for this kind of syntax a lot, particularly loop match (break as the default case of match inside a loop) and loop if else (loop with a manual break condition).

Having to stuff these in nested loops always felt a bit annoying in terms of refactoring and readability. The extra indentation felt especially unnecessary for loop { match { .. } }, and you have to shift around way more elements to convert while { .. } into loop { if { .. } else { break }. As a separate thing, this would also effectively coalesce all non-recursive looping into the loop keyword, which would seem more elegant and scannable than having a separate while keyword.

HOWEVER, I realized not too long after making the post that the implicit break is probably too confusing, especially in a systems language like Rust that tries to be very explicit and clear about the control flow for straightforward readability. Shame, but true....

Your friend, Yokin