Troy Daniels
Troy Daniels

Reputation: 3598

Pass an iterable to a function and iterate twice in rust

I have a function which looks like

fn do_stuff(values: HashSet<String>) {
  // Count stuff
  for s in values.iter() {
    prepare(s);
  }
  // Process stuff 
  for s in values.iter() {
    process(s);
  }
}

This works fine. For a unit test, I want to pass a two value collection where the elements are passed in a known order. (Processing them in the other order won't test the case I am trying to test.) HashSet doesn't guarantee an order, so I would like to pass a Vec instead.

I would like to change the argument to Iterable, but it appears that only IntoIter exists. I tried

fn do_stuff<C>(values: C)
where C: IntoIterator<Item=String>
 {
  // Count stuff
  for s in values {
    prepare(s);
  }
  // Process stuff 
  for s in values {
    process(s);
  }
}

which fails because the first iteration consumes values. The compiler suggests borrowing values, but

fn do_stuff<C>(values: C)
where C: IntoIterator<Item=String>
 {
  // Count stuff
  for s in &values {
    prepare(s);
  }
  // Process stuff 
  for s in values {
    process(s);
  }
}

fails because

the trait Iterator is not implemented for &C

I could probably make something with clone work, but the actual set will be large and I would like to avoid copying it if possible.

Thinking about that, the signature probably should be do_stuff(values: &C), so if that makes the problem simpler, then that is an acceptable solution.

SO suggests Writing a generic function that takes an iterable container as parameter in Rust as a related question, but that is a lifetime problem. I am not having problems with lifetimes.

It looks like How to create an `Iterable` trait for references in Rust? may actually be the solution. But I'm having trouble getting it to compile.

My first attempt is


pub trait Iterable {
    type Item;
    type Iter: Iterator<Item = Self::Item>;
    fn iterator(&self) -> Self::Iter;
}

impl Iterable for HashSet<String> {
    type Item = String;
    type Iter = HashSet<String>::Iterator;
    fn iterator(&self) -> Self::Iter {
        self.iter()
    }
}

which fails with

error[E0223]: ambiguous associated type
   --> src/file.rs:178:17
    |
178 |     type Iter = HashSet<String>::Iterator;
    |                 ^^^^^^^^^^^^^^^^^^^^^^^^^ help: use fully-qualified syntax: `<HashSet<std::string::String> as Trait>::Iterator`

Following that suggestion:

impl Iterable for HashSet<String> {
    type Item = String;
    type Iter = <HashSet<std::string::String> as Trait>::Iterator;
    fn iterator(&self) -> Self::Iter {
        self.iter()
    }
}

failed with

error[E0433]: failed to resolve: use of undeclared type `Trait`
   --> src/file.rs:178:50
    |
178 |     type Iter = <HashSet<std::string::String> as Trait>::Iterator;
    |                                                  ^^^^^ use of undeclared type `Trait`

The rust documents don't seem to include Trait as a known type. If I replace Trait with HashSet, it doesn't recognize Iterator or IntoIter as the final value in the expression.

Implementation of accepted answer

Attempting to implement @eggyal answer, I was able to get this to compile

use std::collections::HashSet;

fn do_stuff<I>(iterable: I)
where
    I: IntoIterator + Copy,
    I::Item: AsRef<str>,
{
    // Count stuff
    for s in iterable {
        prepare(s.as_ref());
    }
    // Process stuff
    for s in iterable {
        process(s.as_ref());
    }
}

fn prepare(s: &str) {
    println!("prepare: {}", s)
}
fn process(s: &str) {
    println!("process: {}", s)
}

#[cfg(test)]
mod test_cluster {
    use super::*;

    #[test]
    fn doit() {
        let vec: Vec<String> = vec!["a".to_string(), "b".to_string(), "c".to_string()];
        let set = vec.iter().cloned().collect::<HashSet<_>>();
        do_stuff(&vec);
        do_stuff(&set);
    }
}

which had this output

 ---- simple::test_cluster::doit stdout ----
prepare: a
prepare: b
prepare: c
process: a
process: b
process: c
prepare: c
prepare: b
prepare: a
process: c
process: b
process: a

Upvotes: 3

Views: 518

Answers (2)

Sven Marnach
Sven Marnach

Reputation: 602515

Iterators over containers can be cloned if you want to iterate the container twice, so accepting an IntoIterator + Clone should work for you. Example code:

fn do_stuff<I>(values: I)
where
    I: IntoIterator + Clone,
{
    // Count stuff
    for s in values.clone() {
        prepare(s);
    }
    // Process stuff
    for s in values {
        process(s);
    }
}

You can now pass in e.g. either a hash set or a vector, and both of them can be iterated twice:

let vec = vec!["a", "b", "c"];
let set: HashSet<_> = vec.iter().cloned().collect();
do_stuff(vec);
do_stuff(set);

(Playground)

Upvotes: 2

eggyal
eggyal

Reputation: 125995

IntoIterator is not only implemented by the collection types themselves, but in most cases (including Vec and HashSet) it is also implemented by their borrows (yielding an iterator of borrowed items). Moreover, immutable borrows are always Copy. So you can do:

fn do_stuff<I>(iterable: I)
where
    I: IntoIterator + Copy,
    I::Item: AsRef<str>,
{
    // Count stuff
    for s in iterable {
        prepare(s);
    }
    // Process stuff 
    for s in iterable {
        process(s);
    }
}

And this would then be invoked by passing in a borrow of the relevant collection:

let vec = vec!["a", "b", "c"];
let set = vec.iter().cloned().collect::<HashSet<_>>();
do_stuff(&vec);
do_stuff(&set);

Playground.

However, depending on your requirements (whether all items must first be prepared before any can be processed), it may be possible in this case to combine the preparation and processing into a single pass of the iterator.

Upvotes: 4

Related Questions