1
\$\begingroup\$

I'm new to Rust and I would like to implement a function to compare 2 collections of strings. The function should compare them as if they are unordered.

In Python, I would implement something like this:

from collections.abc import Iterator

def unordered_eq(a: Iterator[str], b: Iterator[str]):
    a = sorted(a)
    b = sorted(b)
    assert a == b

I then tried implementing something similar in Rust. I'm trying to collect both iterators into new Vec<&str> instances, then sort() each of those vectors, then finally do a simple assert_eq!(a, b).

fn unordered_eq<'a, T, U>(a: T, b: U)
where
  T: Iterator<Item = &'a str>,
  U: Iterator<Item = &'a str>,
{
  let mut a: Vec<&str> = a.collect();
  let mut b: Vec<&str> = b.collect();
  a.sort();
  b.sort();
  assert_eq!(a, b);
}

// Example usage:
struct Item { path: String };

let items: Vec<Item> = ...;

unordered_eq(
  items.iter().map(|x| x.path.as_str()),
  ["hello", "hello2", "hello3", "hello4", "world"].iter().copied(),
)

Is this the correct / idiomatic way to implement this in Rust? How can I improve it?

\$\endgroup\$

2 Answers 2

0
\$\begingroup\$

I'd make three suggestions.

  1. Since the function asserts, name is assert_unordered_eq, right now its surprising given the name that it asserts rather than returns a boolean.
  2. It's typical when taking an iterator as an argument to take IntoIterator instead of Iterator. You call into_iter() on the IntoIterator to get an Iterator and then proceed as normal. The advantage is that your function can then take not only Iterators (which implement IntoIterator in a trivial manner) but also other items like Vec and slices.
  3. Since your function is already generic over the lifetimes of the str, I'd take it a step further and making it generic over any type that supports Ord and Debug

Here's a sample implementation:

fn assert_unordered_eq<X: Ord + std::fmt::Debug, T, U>(a: T, b: U)
where
  T: IntoIterator<Item = X>,
  U: IntoIterator<Item = X>,
{
  let mut a: Vec<_> = a.into_iter().collect();
  let mut b: Vec<_> = b.into_iter().collect();
  a.sort();
  b.sort();
  assert_eq!(a, b);
}

Taking it a step further, for an assert function like this, I'd look at having it do some sort of diff between the two collections and tell me explicitly what's missing/added to make diagnosing test failures (where I assume you'd use this function) easier.

\$\endgroup\$
1
\$\begingroup\$

If you don't care about duplicates, the idiomatic way would be to use a HashSet. This is faster in the cases where it matters and, in my opinion, cleaner:

use std::collections::HashSet;

fn unordered_eq<'a, T, U>(a: T, b: U) -> bool
where
    T: Iterator<Item = &'a str>,
    U: Iterator<Item = &'a str>,
{
    let a: HashSet<_> = a.collect();
    let b: HashSet<_> = b.collect();

    a == b
}

fn main() {
    dbg!(unordered_eq(
        ["hello", "hello2", "hello3", "hello4", "world"]
            .iter()
            .copied(),
        ["hello2", "hello", "hello3", "hello4", "world"]
            .iter()
            .copied(),
    ));
}

If you don't know if there can be duplicates, and you want to check for those too, you can either import multiset and use the HashMultiSet, or keep your current code. I tried making it better with Iter.all(), but it looks like the only true idiomatic way is to try to implement a multiset from scratch, which would be overkill.


As a bit of a side note: I'm not too sure about the type signature you have chosen (taking an iterator as an argument.) It seems a bit weird to me, and I'd intuitively make it take a slice, but I'll keep it in, as that was not a part of the question. :-)

\$\endgroup\$

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service and acknowledge you have read our privacy policy.

Not the answer you're looking for? Browse other questions tagged or ask your own question.