Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

.next() after for ... .zip(): Preventing iterator from being moved

Tags:

rust

I'm new to Rust and haven't quite gotten my head around all of it yet. I've been doing a lot of Rust reading and also playing with math problems. I wrote a function with this signature:

pub fn prime_factors(n: u64) -> Vec<u64> {

So, given n of 30, it should return a vector with values 2, 3, and 5. I built a test that reads:

#[test]
fn prime_factors_test() {
    assert_list_eq([2,3,5].iter(), prime_factors(30).iter());
}

One highlight is that I am comparing a static array to a vector (for learning purposes, this is currently desirable because I'd like to practice generics).

My testing function is the one I'm actually having problems with. To test, the function must iterate through both collections, checking for equality with each index. I used .zip() to do this, but if the collections are uneven, the iterator will only exhaust through the index of the shorter one. So after zip, I want to check if either of the iterators has extra elements.

The following does not compile:

fn assert_list_eq<I, T>(mut expected: I, mut actual: I)
    where I: Iterator<Item=T> + Clone,
          T: PartialEq + Debug {
    for (e, a) in expected.zip(actual) {
        assert_eq!(e, a);
    }
    // fail if there are any "leftovers"
    assert_eq!(None, expected.next());
    assert_eq!(None, actual.next());
}

I think the loop (or maybe the setup for the loop) moves/consumes the iterators, and they can't be used after the loop. But I want to check on the status of those iterators post loop. I think I know what's wrong, I just don't know how to make it right. I'm almost certainly having a conceptual problem.

As a workaround I did this, but I feel like I'm embracing an anti-pattern by using the .clone() just to make things work.

fn assert_list_eq<I, T>(expected: I, actual: I)
    where I: Iterator<Item=T> + Clone,
          T: PartialEq + Debug {
    assert!(expected.clone().count() == actual.clone().count()); // desperation
    for (e, a) in expected.zip(actual) {
        assert_eq!(e, a);
    }
}

BTW (acknowledging XY Problem): I'd be happy to learn of a builtin assertion to compare two ordered collections, but that's not really what I'm confused about.

like image 795
brian_o Avatar asked Oct 22 '25 14:10

brian_o


1 Answers

zip takes its arguments by value, so that's why they're moved and you can't use them afterwards.

However, there's a workaround: there's a by_ref method that returns a mutable reference to the iterator, and the trick is that &mut I where I: Iterator also implements Iterator. You can thus apply .by_ref() to both of your iterators before you pass them to zip():

fn assert_list_eq<I, T>(mut expected: I, mut actual: I)
    where I: Iterator<Item=T> + Clone,
          T: PartialEq + Debug {
    for (e, a) in expected.by_ref().zip(actual.by_ref()) {
        assert_eq!(e, a);
    }
    // fail if there are any "leftovers"
    assert_eq!(None, expected.next());
    assert_eq!(None, actual.next());
}

That said, the standard library already provides all the necessary tools to compare a fixed-size array with a Vec. It's a bit tricky though, because PartialEq is generic on its right-hand side argument and there are not enough implementations for it to work the "obvious" way. Here's one way to do it:

#[test]
fn prime_factors_test() {
    assert_eq!([2,3,5][..], prime_factors(30)[..]);
}

[..] forces the expressions to be converted to slices, using the Index<RangeFull> trait (.. is a shorthand for RangeFull).

Another way to do it is to just swap the operands, then you don't need [..].

#[test]
fn prime_factors_test() {
    assert_eq!(prime_factors(30), [2,3,5]);
}

That's because the standard library provides impl PartialEq<[T; 3]> for Vec<T>, but not impl PartialEq<Vec<T>> for [T; 3].

like image 144
Francis Gagné Avatar answered Oct 25 '25 16:10

Francis Gagné



Donate For Us

If you love us? You can donate to us via Paypal or buy me a coffee so we can maintain and grow! Thank you!