2

I've been working through the 2020 Advent of Code in Rust, trying to produce solutions involving fairly generic functions, which could (in theory) be reused elsewhere without having to worry too much about types. For day 2, the problem involves checking each line of a file to make sure that it is a string conforming to some given condition. My current approach is to define a function similar to the following:

fn filter_lines<'a>(lines: impl IntoIterator<Item = &'a str>) -> Vec<&'a str> {
    lines
        .into_iter()
        .filter(|line| verify_line(line))
        .collect()
}

Here, verify_line is just some function which takes an &str and produces a boolean depending on whether the line conforms to a certain specification.

The issue is that this function appears to necessarily consume the iterator. I've tried rewriting it as follows to use a reference to lines, and clone lines before operating on this, but this code won't compile:

fn filter_lines<'a>(lines: &impl IntoIterator<Item = &'a str>) -> Vec<&'a str> {
    let lines = lines.clone();
    lines
        .into_iter()
        .filter(|line| verify_line(line))
        .collect()
}

The compiler produces the following error:

error[E0507]: cannot move out of `*lines` which is behind a shared reference
  --> src/main.rs:40:5
   |
40 |     lines
   |     ^^^^^ move occurs because `*lines` has type `impl IntoIterator<Item = &'a str>`, which does not implement the `Copy` trait

I think I understand why doing this isn't allowed, but I'm not sure how to idiomatically define a function such as filter_lines which won't consume the iterator. So, what would be a better way to implement this filter_lines function such that it can be called as follows?

let some_str: String = get_file_contents();
let lines = some_str.lines();
println!("Filtered lines: {:?}", filter_lines(&lines));
println!("All lines: {:?}", lines.collect::<Vec<&str>>());
Endzeit
  • 4,810
  • 5
  • 29
  • 52
  • I should have clarified: I've solved the problem posed in Advent of Code, I'm not trying to "cheat" on solving it, my question is just for my own interest :) –  Dec 02 '20 at 13:19
  • Note that the `IntoIterator` is designed explicitly to create iterators that are consumed. Compare the `into_iter()` and `iter()` methods, for example on Vec. `iter()` doesn't consume the input, but is actually defined by the generic slice type. This is due to the fact that if you don't consume an iterator, you merely reference it, therefore it needs an owner. – SirDarius Dec 02 '20 at 13:37

2 Answers2

3

The issue for me is that this function appears to necessarily consume the iterator.

That's going to happen in every case, because that's what IntoIterator does. So if you take an IntoIterator input, it's up to the caller to set it up correctly e.g. Vec has impl IntoIterator for Vec<T> and impl IntoIterator for &[T], so the caller can pass in &v — though that requires some adaptation anyway as the IntoIterator for &[T] yields Iterator<Item=&T>, so here it's an Iterator<Item=&&str> which wouldn't work.

But since every Iterator also implements IntoIterator as well that's not really an issue: the caller can just do some adaptation. Anyway my point is the function works as-is, it's the caller which needs changes.

Now as to the specific snippet:

let some_str: String = get_file_contents();
let lines = some_str.lines();
println!("Filtered lines: {:?}", filter_lines(&lines));
println!("All lines: {:?}", lines.collect::<Vec<&str>>());

that can not work, because Lines is an Iterator it will be consumed by any iteration: Rust iterators are not "replayable", it's only the "iterables" which may be. But it also doesn't really make much sense to try and reuse lines(), what would that even do?

  • it could redo the entire thing, but then just call some_str.lines() twice and create two iterators in parallel

  • or it could memoise the entire thing, but then just collect() the Lines upfront and iterate on the subsequent Vec however you need, that's basically what you're expecting, and since you're collecting to a vec anyway to print all the lines you might as well do that first:

    let some_str: String = String::from("foo\nbar\nbaz");
    let lines = some_str.lines().collect::<Vec<_>>();
    // Vec<&str> -> Iterator<Item=&&str> -> Iterator<Item=&str>
    println!("Filtered lines: {:?}", filter_lines(lines.iter().map(|s| *s)));
    println!("All lines: {:?}", lines);
    
Masklinn
  • 34,759
  • 3
  • 38
  • 57
  • Thank you so much for the insightful answer, I really appreciate it! That makes total sense, I think my intuition about iterators was a bit off beforehand. –  Dec 02 '20 at 13:47
1

Not saying this is the idiomatic way to do this, but in your snippet, you are actually cloning the reference because of which it doesn't work.

fn filter_lines<'a>(lines: &impl IntoIterator<Item = &'a str>) -> Vec<&'a str> {
    // Here you are creating a clone of the reference, not iterator
    let lines = lines.clone();
    lines
        .into_iter()
        .filter(|line| verify_line(line))
        .collect()
}

To clone the iterator, you need to impose a trait bound for Clone as well:

fn filter_lines<'a>(lines: &(impl IntoIterator<Item = &'a str> + Clone)) -> Vec<&'a str> {
    let lines = lines.clone();
    lines
        .into_iter()
        .filter(|line| verify_line(line))
        .collect()
}

Because Lines implements Clone as well, this will work.

Playground

Mihir Luthra
  • 6,059
  • 3
  • 14
  • 39
  • Ah, thank you! I'm going to change my approach to this problem a little because of Masklinn's answer, but this is definitely helpful in understanding the error the compiler raised :) Do you know why auto-dereferencing doesn't apply here? –  Dec 02 '20 at 13:56
  • @user14749127, `lines.clone()` will work here as well as long as you specify bounds for `Clone`. Both reference and your iterator implement `Clone`. In second case when you specified bound for `Clone`, due to type inference rust came to the conclusion you mean iterator's clone. But in first case, as your iterator didn't have any `clone()` method so it sticked with reference's clone. – Mihir Luthra Dec 02 '20 at 14:08