Help translating for loop into iterator chain

How can I convert the for loop below into an iterator chain that can be assigned to has_desc without intermediaries variables?

let has_desc: bool = ...

for line in buffer.lines() {
  let line = line?;

  if line.contains("## Description") {
      has_desc = true;
      break;
  }
}
1 Like

Something like let has_desc = buffer.lines().any(|line| line.contains("## Description"));?

Edit: Never mind. Missed the let line = line?. That is a lot harder to handle.

let has_desc: bool = buffer.lines().map_while(Result::ok).any(|line| line.contains("## Description"));

Explanation:

  1. map_while() basically "unwraps" the Results until it encounters the Err variant in which case it terminates the iterator, similar to your early return using ?.
  2. any() searches the resulting iterator for any match of the predicate, i.e. whether any call of the closure returns true.
3 Likes

I would use filter_map() and any() for this:

let has_desc =
    buffer.lines()
          .filter_map(Result::ok)
          .any(|line| line.contains("## Description"));

I haven't tested this though.

1 Like

Both these suggestions would discard errors rather than propagate them to the caller while mine doesn't work with errors at all.

2 Likes

That's my struggle, how to propagate the error.

Ah, that is true. I think the best approach might be to hack together something with try_fold() (propagating any error and doing a Boolean or in the closure), if you want to avoid collecting into an intermediate Vec to handle the errors upfront. It might lose the early exit when true is returned though (but so does any() I think).

Edit: @nerditation has provided an example of the try_fold() approach below

2 Likes

try_fold() can achieve equivalent result, but is not as efficient as the original, due to no early break on success case. also, I feel a bit cheaty by using fold() and closure.

    let has_desc = std::io::stdin().lines().try_fold(false, |found, line| {
        // type annotation needed for the question mark operator
        std::io::Result::Ok(found || line?.contains("## Description"))
    })?;
4 Likes

I'm pretty sure any() (and all() too) is short-circuiting.

2 Likes

I think I'm going to stick with the normal for loop for the sake of clarity, since it seems there's no way to be concise and clear using iterator chaining here. Thank you, everybody.

1 Like

I would recommend that as well. Anyways, here is a solution (neither concise nor clear) that short circuits and propagates the error:

buffer.lines()
        .find_map(|line| line.map(|line| line.contains("## Description").then_some(())).transpose())
        .transpose()?
        .is_some()
3 Likes

Itertools::process_results can offer a nice alternative:

use itertools::Itertools;

let has_desc: bool = buffer.lines().process_results(|mut lines| {
    lines.any(|line| line.contains("## Description")) //
})?;

(the // prevents rustfmt from changing the formatting into its – IMO much worse – default here)

7 Likes

With sufficient effort, try_for_each or try_fold will work:

use std::io::BufRead;
use std::ops::ControlFlow;
pub fn has_desc(buffer: impl BufRead) -> std::io::Result<bool> {
    buffer
        .lines()
        .try_for_each(|line| match line {
            Err(e) => ControlFlow::Break(Err(e)),
            Ok(line) => {
                if line.contains("## Description") {
                    ControlFlow::Break(Ok(true))
                } else {
                    ControlFlow::Continue(())
                }
            }
        })
        .break_value()
        .unwrap_or(Ok(false))
}

including the early short-circuiting when it's found.

But you're right to

3 Likes