Breaking out of iterator

Hello everyone, happy thanksgiving!

Here's some code:

fn main() {
 (1..=10).for_each(|x| dummy(x))
}

fn dummy(a: i32) {
    println!("{}", a);
    // if a == 5 {
    //    break;
    // }
}

So what I want to do is break out of an iterator. In this simplified test code, I want to stop printing the contents of the range when the number is 5. How would I do this? As in other languages, the break keyword is only available in loops.

Playground Link

1 Like

It's not very convenient to use iterator approach in some cases, and sometimes it doesn't work (await in the closure for example).
A simple loop will be good.

for_each (aka the simplest fold) always runs to the end of the iterator -- that's why it takes self, since when it's done there's nothing more useful to do with the iterator.

The version that can end early is try_for_each (aka the simplest try_fold), which looks at the result of the closure -- and takes &mut self since it can stop early so you might want to use the iterator again after it.

Here's my take on a tidy version of that, using a couple nightly-only features:

fn main() {
   (1..=10).try_for_each(|a| try {
       println!("{}", a);
       if a == 5 { return ControlFlow::BREAK; }
   });
}

There are also ways of doing the same on stable, though I find them a bit less clear.

† Assuming no panic, nobody pulls the plug, etc, of course

5 Likes

Interesting... can you explain how this works?

There are some ways around this, but using some of these clever tricks hurts the readability of the code.
"Use the right tool for the right job".
Iterator adapters aren't really the right tool if all you want to do for each item in the iterator is run some code solely for its side effects. If you're not adapting the value of each item in the iterator in some way, iterator adapters probably aren't the right tool.

for x in (1..=10) {
    println!("{}", x);
    if x == 5 {
        break;
    }
}

is so simple and it just works.

7 Likes

Hm... my apologies, but what is an adapter? I started learning a few weeks ago so most things are very foreign to me.

I might butcher this explanation... but very loosely speaking: An iterator adapter is something that takes an iterator as an input, and changes that iterator in some way. Let's use (1..=10) as our iterator for the example.

for i in 1..=10 {
    println!("{}", i);
}

will just print the numbers 1 through to 10. But let's say we only wanted to print the even numbers. We could do

for i in 1..=10 {
    if i % 2 == 0 {
        println!("{}", i);   
    }
}

to do nothing for the iterations where i is odd. Or, we could filter out the iterations where i is odd:

for i in (1..=10).filter(|x| x % 2 == 0) {
    println!("{}", i);
}

The .filter() call here is an iterator adapter that takes (1..=10) as an input, and returns a new iterator that skips the odd numbers. You can often use multiple iterator adapters together. This one will square the even numbers from 1 to 10. The .map() is an iterator adapter:

for i in (1..=10).filter(|x| x % 2 == 0).map(|x| x * x) {
    println!("{}", i);
}

And here's one that uses the .sum() adaptor:

let sum: i32 = (1..=10).filter(|x| x % 2 == 0).map(|x| x * x).sum();
println!("{}", sum);

The sum iterator adaptor takes an iterator as an input and uses it to calculate and return a sum, but doesn't return another iterator.
Same case with for_each except that for_each doesn't return anything.

2 Likes

I recommend you read the std::iter docs from top to bottom. I recently did and realized that I should have read them a long time ago.

1 Like

Strictly speaking, sum (like for_each, fold, collect etc.) is not an adapter, but a consumer - i.e. the method which converts an iterator into something which is not an iterator. The crucial difference is in the fact that iterators (and therefore iterator adapters) are lazy - the iteration only happens inside the consumer.

3 Likes

If there's a break condition you can check before the for_each, you can take_while:

(1..=10).take_while(|&x| x <= 5).for_each(|x| dummy(x))
5 Likes

I especially feel that with the nightly-using version above. The block only conditionally returning an enum while falling through and returning () in the other case is extremely confusing, for one.

Curiosity: is that because of the nightly features, or something else? Here's a structurally-identical stable version of it:

fn main() {
   (1..=10).try_for_each(|a| Some({
       println!("{}", a);
       if a == 5 { return None; }
    }));
}

Yes, concretely it's the absence (or rather, implicit presence) of an outer Some (or is it Ok? I can't tell, that's why it bothers me), which is apparent in the stable version.

So the complaint is not so much about the nightly feature, but about type inference then? Because this works on stable just fine too (with an appropriate trait):

fn main() {
   (1..=10).try_for_each(|a| Yolo::whatever({
       println!("{}", a);
       if a == 5 { return None; }
    }));
}

(The nightly version has syntactically-visible [aka not implicit] wrapping unto a inferred type, just like this stable one.)

Well, sort of, but not mainly. Your latest example is still better than a plain try in that I can go to the docs or the definition of Yolo::whatever and see what it's doing, and I know it's likely implemented for Option. So at least there's a concrete trait, even if the type is inferred. try doesn't tell me any of that information, I have to know what exactly it does.

sometimes that old C -- for (setup;check;change) {...}
thing is just easy to think with.

This topic was automatically closed 90 days after the last reply. We invite you to open a new topic if you have further questions or comments.