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.

2 Likes

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

1 Like

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

Sometimes you just cannot make a function, because it would be passing like 10 arguments, especially in embedded, where you are trying to pass all resources manually, without using allocator to get some memory. It's really very common

This topic was automatically closed 90 days after the last reply. New replies are no longer allowed.