Is try_fold the preferred way to stop an infinite iteration or are there more idiomatic alternatives?

Viewed 494

I'm looking for ways to interrupt iteration of an infinite iterator. I found that try_fold accomplishes my objective. However, that requires doing the awkward thing of returning an Err on the successful case. What I'm looking to understand is if this is an idiomatic way of doing things. The only other way I can think of doing this is using a regular for, or something like a find while keeping external state (which feels even weirder!). I know in clojure there's reduced, but I couldn't find an equivalent for rust.

Here's a minimum viable example. The example cycles around the initial Vec, summing each item as it goes, and stops at the first sum larger than 10. I.e. it returns 12, because 1 + 5 - 3 + 1 + 5 - 3 + 1 + 5 = 12:

fn main() {
    let seed = vec![1, 5, -3];

    let res = seed.iter().cycle().try_fold(0, |accum, value| {
        let next = accum + value;
        if next > 10 {
            Err(next)
        } else {
            Ok(next)
        }
    });
    if let Err(res) = res {
        println!("{:?}", res);
    } else {
        unreachable!();
    }
}

(playground link)

The part that feels weird to me is the if let Err(res) = res being the positive condition and really the only way out of the cycle (hence why the other branch is unreachable).

Is there a 'better' way?

4 Answers

Simple, just iterate normally

fn main() {
    let seed = vec![1, 5, -3];
    let mut accum = 0;
    for value in seed.iter().cycle() {
        accum += value;
        if accum > 10 {
            break;
        }
    }
    println!("{:?}", accum)
}

You can use scan and find:

fn main() {
    let seed = vec![1, 5, -3];
    let res = seed
        .iter()
        .cycle()
        .scan(0, |accum, value| {
            *accum += value;
            Some(*accum)
        })
        .find(|&x| x > 10)
        .unwrap();
    println!("{:?}", res);
}

This is actually slightly shorter than using fold_while from itertools, which would look like this:

use itertools::{FoldWhile::{Continue, Done}, Itertools};

fn main() {
    let seed = vec![1, 5, -3];
    let res = seed
        .iter()
        .cycle()
        .fold_while(0, |accum, value| {
            let next = accum + value;
            if next > 10 {
                Done(next)
            } else {
                Continue(next)
            }
        })
        .into_inner();
    println!("{:?}", res);
}

Consider using itertools::fold_while if you don't mind using a well established external library. There might be other useful extensions if you like this style of programming.

As of today, there is no obvious answer to your question which is actually very surprising. Like @peter-hall suggested, scan is the idiomatic functional answer to the problem but Rust's scan is known to be ugly to use.

In my opinion try_fold remains the rustiest option. Here are my combined suggestions to make looped folds more satisfying (pick one or more changes!) :

let res = {
    use Result::{Err as Break, Ok as Next};
    seed.iter()
        .cycle()
        .try_fold(0, |accum, value| match accum + value {
            res if res > 10 => Break(res),
            next => Next(next),
        })
        .unwrap_err()
};

Using a match helps naming the cases better than if/else in my opinion. The Result types aliases also give some semantics describing the breaking indention.

If you don't want to use unwrap_err, someone suggested to replace it by

let res = iter.try_fold(...).unwrap_or_else(|res| res);

It's also possible to use an irrefutable pattern:

let (Ok(res) | Err(res)) = iter.try_fold(...);

If it were for my own code, I'd keep things as simple and vanilla as possible:

let res = seed
    .iter()
    .cycle()
    .try_fold(0, |accum, value| match accum + value {
        res if res > 10 => Err(res),
        acc => Ok(acc),
    })
    .unwrap_err();

Reading .unwrap_err() is clear enough to me because it means that the Err is the return type we expect. That's my best shot, cheers!

Related