问题
I want to pass Iterators to a function, which then computes some value from these iterators. I am not sure how a robust signature to such a function would look like. Lets say I want to iterate f64. You can find the code in the playground: https://play.rust-lang.org/?version=stable&mode=debug&edition=2018&gist=c614429c541f337adb102c14518cf39e
My first attempt was
fn dot(a : impl std::iter::Iterator<Item = f64>,b : impl std::iter::Iterator<Item = f64>) -> f64 {
a.zip(b).map(|(x,y)| x*y).sum()
}
This fails to compile if we try to iterate over slices
So you can do
fn dot<'a>(a : impl std::iter::Iterator<Item = &'a f64>,b : impl std::iter::Iterator<Item = &'a f64>) -> f64 {
a.zip(b).map(|(x,y)| x*y).sum()
}
This fails to compile if I try to iterate over mapped Ranges. (Why does the compiler requires the livetime parameters here?)
So I tried to accept references and not references generically:
pub fn dot<T : Borrow<f64>, U : Borrow<f64>>(a : impl std::iter::Iterator::<Item = T>, b: impl std::iter::Iterator::<Item = U>) -> f64 {
a.zip(b).map(|(x,y)| x.borrow()*y.borrow()).sum()
}
This works with all combinations I tried, but it is quite verbose and I don't really understand every aspect of it.
Are there more cases?
What would be the best practice of solving this problem?
回答1:
There is no right way to write a function that can accept Iterator
s, but there are some general principles that we can apply to make your function general and easy to use.
- Write functions that accept
impl IntoIterator<...>
. Because allIterator
s implementIntoIterator
, this is strictly more general than a function that accepts onlyimpl Iterator<...>
. Borrow<T>
is the right way to abstract overT
and&T
.- When trait bounds get verbose, it's often easier to read if you write them in
where
clauses instead of in-line.
With those in mind, here's how I would probably write dot
:
fn dot<I, J>(a: I, b: J) -> f64
where
I: IntoIterator,
J: IntoIterator,
I::Item: Borrow<f64>,
J::Item: Borrow<f64>,
{
a.into_iter()
.zip(b)
.map(|(x, y)| x.borrow() * y.borrow())
.sum()
}
However, I also agree with TobiP64's answer in that this level of generality may not be necessary in every case. This dot
is nice because it can accept a wide range of arguments, so you can call dot(&some_vec, some_iterator)
and it just works. It's optimized for readability at the call site. On the other hand, if you find the Borrow
trait complicates the definition too much, there's nothing wrong with optimizing for readability at the definition, and forcing the caller to add a .iter().copied()
sometimes. The only thing I would definitely change about the first dot
function is to replace Iterator
with IntoIterator
.
回答2:
You can iterate over slices with the first dot
implementation like that:
dot([0, 1, 2].iter().cloned(), [0, 1, 2].iter().cloned());
(https://doc.rust-lang.org/std/iter/trait.Iterator.html#method.cloned) or
dot([0, 1, 2].iter().copied(), [0, 1, 2].iter().copied());
(https://doc.rust-lang.org/std/iter/trait.Iterator.html#method.copied)
Why does the compiler requires the livetime parameters here?
As far as I know every reference in rust has a lifetime, but the compiler can infer simple it in cases. In this case, however the compiler is not yet smart enough, so you need to tell it how long the references yielded by the iterator lives.
Are there more cases?
You can always use iterator methods, like the solution above, to get an iterator over f64
, so you don't have to deal with lifetimes or generics.
What would be the best practice of solving this problem?
I would recommend the first version (and thus leaving it to the caller to transform the iterator to Iterator<f64>
), simply because it's the most readable.
来源:https://stackoverflow.com/questions/57543399/how-to-properly-pass-iterators-to-a-function-in-rust