How to make a general function that groups an (generic) IntoIterator's items in Rust?

Viewed 133

I am currently learning Rust and came across a question: "How could I have a function that takes a collection and returns an iterator of its elements grouped by a given number?"
This is what I mean:

let v = vec![1, 2, 3, 4, 5, 6, 7, 8];
// `result` should be an iterator with
// the elements [1,2,3], [4,5,6], [7,8]
let result = myfunction(&v, 3); // groups of three (while possible)

This is easy when I implement the function with elements "aligned left" (we always fill the groups from the left to the right. If a group is missing one element, it will be the rightmost one).

However, to make a version of this function "aligned right", I have to know the amount of elements the iterator has. Here I experimented a bit:

#[allow(unused_variables)]
fn iter_from_iter<T: IntoIterator>(data: T) -> T::IntoIter
{
    let it = data.into_iter();
    it
}

fn main() {
    let v = vec![1,2,3,4,5,6,7,8];
    let it = (&v).into_iter(); // doesnt take ownership from `v`
    let c = (&v).into_iter().count(); // here neither
    println!("Iterator (returned): {:?}",
        iter_from_iter(&v) // need to pass by reference, otherwise the ownership is moved
    );
    println!("Vector: {:?}", v);
    println!("Iterator: {:?}", it);
    println!("Count: {:?}", c);
}

/*
 * output:
 * Iterator (returned): Iter([1, 2, 3, 4, 5, 6, 7, 8])
 * Vector: [1, 2, 3, 4, 5, 6, 7, 8]
 * Iterator: Iter([1, 2, 3, 4, 5, 6, 7, 8])
 * Count: 7
 */

I can even add the following lines to main():

    let slice = "hello, world";
    let chars = slice.chars();
    let c = slice.chars().count();
    println!("Chars Iterator: {:?}", chars);
    println!("Chars Iterator (returned): {:?}", iter_from_iter(chars));
    println!("Count: {:?}", c);
   /*
    * output:
    * Chars Iterator (returned): Chars(['h', 'e', 'l', 'l', 'o', ' ', 'w', 'o', 'r', 'l', 'd'])
    * Chars Iterator: Chars(['h', 'e', 'l', 'l', 'o', ' ', 'w', 'o', 'r', 'l', 'd'])
    * Count: 12
    */

As we can see, the function iter_from_iter() can make an iterator from our argument. But if we change the function iter_from_iter() definition to the following

fn iter_from_iter<T: IntoIterator>(data: T) -> (T::IntoIter, usize)
{
    let it = data.into_iter();
    let count = data.into_iter().count();
    (it, count)
}

The compiler complains

Compiling my-test v0.1.0 (/home/arch/codigos/rust/my-test)
error[E0382]: use of moved value: `data`
   --> src/main.rs:5:17
    |
2   | fn iter_from_iter<T: IntoIterator>(data: T) -> (T::IntoIter, usize)
    |                                    ---- move occurs because `data` has type `T`, which does not implement the `Copy` trait
3   | {
4   |     let it = data.into_iter();
    |                   ----------- `data` moved due to this method call
5   |     let count = data.into_iter().count();
    |                 ^^^^ value used here after move
    |
note: this function takes ownership of the receiver `self`, which moves `data`
   --> /home/arch/.rustup/toolchains/stable-x86_64-unknown-linux-gnu/lib/rustlib/src/rust/library/core/src/iter/traits/collect.rs:233:18
    |
233 |     fn into_iter(self) -> Self::IntoIter;
    |                  ^^^^
help: consider further restricting this bound
    |
2   | fn iter_from_iter<T: IntoIterator + Copy>(data: T) -> (T::IntoIter, usize)
    |                                   ^^^^^^

error: aborting due to previous error

For more information about this error, try `rustc --explain E0382`.
error: could not compile `my-test`

To learn more, run the command again with --verbose.

If I make the suggested changes (require T to implement the Copy trait), the vector case now compiles, but if I insert those lines about Chars again, the code doesn't compile, because

the trait Copy is not implemented by Chars<'_>

If I then remove the Copy trait bound and change the function signature to fn iter_from_iter<T: IntoIterator>(data: &T) -> (T::IntoIter, usize), the compiler complains again

   Compiling my-test v0.1.0 (/home/arch/codigos/rust/my-test)
error[E0507]: cannot move out of `*data` which is behind a shared reference
 --> src/main.rs:4:14
  |
4 |     let it = data.into_iter();
  |              ^^^^ move occurs because `*data` has type `T`, which does not implement the `Copy` trait

error[E0507]: cannot move out of `*data` which is behind a shared reference
 --> src/main.rs:5:17
  |
5 |     let count = data.into_iter().count();
  |                 ^^^^ move occurs because `*data` has type `T`, which does not implement the `Copy` trait

error: aborting due to 2 previous errors

For more information about this error, try `rustc --explain E0507`.
error: could not compile `my-test`

To learn more, run the command again with --verbose.

I then tried some things like using (&data).into_iter() instead of data.into_iter(), but nothing seems to solve my problem...

I searched on the internet a lot about Iterators and IntoIterators (including the docs), but couldn't find a solution. It's been days now.

Could someone more experienced, please, tell me what is needed for:

  • Taking an IntoIterator as argument;
  • not taking the ownership of it;
  • getting the result from its into_iter() inside my function;
  • and still being able to call into_iter().count() inside my function.

So that I could have something like this (here I use ellipsis where I don't know what should be the right type):

// make groups of `size` elements from an IntoIterator
fn <...>myfunction(data: ..., size: usize) -> ... {
    let iter = data.into_iter();
    let len = data.into_iter().count(); // `iter` must still be valid
    // -- gets elements from `iter` and do the rest of operations --
}

Hope you can help me. Thank you in advance!

Obs.: I thought about making T::IntoIter have the ExactSizeIterator so that I could use the function len() on the Iterator obtained from data.into_iterator(), but it doesn't work either. That's because Chars doesn't implement it...

1 Answers

I created a version with a bit of unsafe and nightly features and a version with a Vec<T> instead of arrays:


#![feature(maybe_uninit_array_assume_init)]
use std::mem::MaybeUninit;

struct Chunks<T, const N: usize> {
    iter: Box<dyn Iterator<Item = T>>,
}

impl<T, const N: usize> Iterator for Chunks<T, N> {
    type Item = [Option<T>; N];
    fn next(&mut self) -> Option<Self::Item> {
        let mut res: [MaybeUninit<Option<T>>; N] = unsafe { MaybeUninit::uninit().assume_init() };

        for elem in &mut res[..] {
            unsafe { elem.as_mut_ptr().write(self.iter.next()) };
        }

        let res = unsafe { MaybeUninit::array_assume_init(res) };

        if res.iter().filter(|e| e.is_some()).count() >= 1 {
            Some(res)
        } else {
            None
        }
    }
}

// make groups of `size` elements from an IntoIterator
fn chunks<T, I: IntoIterator<Item = T> + 'static, const N: usize>(
    data: I,
) -> impl Iterator<Item = [Option<T>; N]> {
    Chunks {
        iter: Box::new(data.into_iter()),
    }
}

Playground

Notice that the type for [T; 2] is different from [T; 3], so you could have to wrap everything in an Option, or use a dynamically sized vector version:

struct Chunks<T> {
    iter: Box<dyn Iterator<Item = T>>,
    n: usize
}

impl<T> Iterator for Chunks<T> {
    type Item = Vec<T>;
    fn next(&mut self) -> Option<Self::Item> {
        let mut res = Vec::new();
        for _ in 0..self.n {
            match self.iter.next() {
                Some(item) => res.push(item),
                _ => break,
            }
        }
        if res.is_empty() {
            None
        } else {
            Some(res)
        }
    }
}

// make groups of `size` elements from an IntoIterator
fn chunks<T, I: IntoIterator<Item = T> + 'static>(
    data: I,
    n: usize
) -> impl Iterator<Item = Vec<T>> {
    Chunks {
        iter: Box::new(data.into_iter()),
        n,
    }
}

Playground

Related