Is there a way to distinguish normal loop termination from break termination in Rust?
Asked Answered
S

6

13

Idiomatic for/else in rust:

In Python, I can use for/else to check whether or not a for loop terminated at a break statement or finished 'normally':

prod = 1
for i in range(1, 10):
    prod *= i
    if prod > 123:
        break
else:
    print("oops! loop terminated without break.")

Is there a similar way to do that in Rust? This is the closest I have come, but it is not particularly similar.

let mut prod = 1u64;
let mut found = false;
for i in 1..10 {
    prod *= i;
    if prod > 123 {
        found = true;
        break;
    }
}
if !found {
    println!("oops! loop terminated without break.");
}

There seems to be a discussion about this on rust internals; however, that is more about future possibilities than what is idiomatic.

In Python, idiomatic code is called pythonic - is there a similar word for idiomatic Rust code?

Seawright answered 4/7, 2022 at 6:56 Comment(3)
Not an answer, but when I face this need, I immediately think whether I really need this - or an iterator or maybe a loop with break-with-value will do better.Rubel
@ChayimFriedman would you have any suggestion in this particular example (in my real case i iterate over a Vec<u64> and not a range)? i am not interested in the prod, but in the index of the vector when the product becomes too large.Seawright
I would probably use enumerate().try_fold(). I won't post that as an answer since the question was in general.Rubel
R
5

Since Rust 1.65, you can break out of blocks, optionally with a value. Cases like that were even mentioned in the RFC as a motivation:

let value = 'found: {
    for i in 1..2 {
        prod *= i;
        if prod > 123 {
            break 'found Some(prod);
        }
    }
    println!("oops! loop terminated without break.");
    None
};

Playground.

Rubel answered 4/7, 2022 at 19:52 Comment(3)
great, thank you! once this feature is stable this should become the accepted answer.Seawright
@hiroprotagonist Stabilized for Rust 1.65!Rubel
rust 1.65 with that feature is now out! blog.rust-lang.org/2022/11/03/…Seawright
L
4

A simple None initialized value provides a reasonably convenient way to do this:

let mut prod = 1u64;
let mut value = None;
for i in 1..10 {
    prod *= i;
    if prod > 123 {
        value = Some(prod);
        break;
    }
}
if value.is_none() {
    println!("oops! loop terminated without break.");
}
Lindquist answered 4/7, 2022 at 8:0 Comment(2)
It may seem silly coming from me, as I wrote two answers here, but I think this is The Way.Sheetfed
very nice indeed! although the difference to my original attempt is small, i prefer this one.Seawright
S
3

I don't know how idiomatic it is but sometimes I write this:

fn main() {
    let mut prod = 1u64;
    let value = 'found: loop {
        for i in 1..2 {
            prod *= i;
            if prod > 123 {
                break 'found Some(prod);
            }
        }
        println!("oops! loop terminated without break.");
        break 'found None;
    };
    //Here value is Option<u64> with the result.
}

That is, an outer labelled loop that can be broken from inside when you are ready. You can even give it a type with the break label value` syntax.

Sheetfed answered 4/7, 2022 at 7:13 Comment(5)
this works. although the outer loop does not look that nice. even if it never really loops...Seawright
@hiroprotagonist: I agree it is not ideal. I tend to forget the ending break and have an infinite loop.Sheetfed
clippy agrees with both of us: error: this loop never actually loops. had to decorate it with #[allow(clippy::never_loop)].Seawright
@hiroprotagonist: If you don't need the break value you can loop with for _ in [()], but this time Rustc itself complains with: "warning: for loop over a single element".Sheetfed
label-break-value. Example.Rubel
Y
3

Usually you can translate to some iterator pattern instead of the for loop:

fn main() {
    let mut prod = 1u64;
    if let Some(i) = (1..10)
        .filter_map(|i| {
            prod *= i;
            if prod > 123 {
                Some(i)
            } else {
                None
            }
        })
        .next()
    {
        println!("item found for {i}");
    }
}

Playground

Yellowbird answered 4/7, 2022 at 8:24 Comment(1)
also nice! i tried to make an iterator-based approach work but found nothing. thanks!Seawright
S
2

I have seen people use a temporal locale function, Javascript style:

fn main() {
    let mut prod = 1u64;
    let value = (|| {
        for i in 1..10 {
            prod *= i;
            if prod > 123 {
                return Some(prod);
            }
        }
        println!("oops! loop terminated without break.");
        None
    })();
}

You define a temporary function and immediately call it, this way you can have return inside. This trick is also used to propagate errors with ?, while the try {} statement is not stable.

Sheetfed answered 4/7, 2022 at 7:28 Comment(1)
also a good idea! but again... a bit verbose (may just seem to me that way because i am much more used to python...).Seawright
V
-5

How about this?

let result = {
    for i in 1..10 {
        prod *= i;
        if prod > 123 {
            Ok(())
        }
    }
    Err(())
}
Vendace answered 4/7, 2022 at 7:7 Comment(3)
But your variable result is never used because you return finishes the function, not the inner expression.Sheetfed
@Sheetfed Thanks. Its been a while since I've done any actual rust...Vendace
tried it. it does not compile: error[E0308]: mismatched type.Seawright

© 2022 - 2024 — McMap. All rights reserved.