How to properly pass Iterators to a function in Rust

别来无恙 提交于 2021-01-28 07:30:31

问题


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 Iterators, but there are some general principles that we can apply to make your function general and easy to use.

  1. Write functions that accept impl IntoIterator<...>. Because all Iterators implement IntoIterator, this is strictly more general than a function that accepts only impl Iterator<...>.
  2. Borrow<T> is the right way to abstract over T and &T.
  3. 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

标签
易学教程内所有资源均来自网络或用户发布的内容,如有违反法律规定的内容欢迎反馈
该文章没有解决你所遇到的问题?点击提问,说说你的问题,让更多的人一起探讨吧!