6

I'm trying to iterate over a slice broken into chunks, and return a tuple with the nth element of each chunk.

Example:

&[1,2,3,4,5,6,7,8,9]

I'd like to break this into chunks of size 3, and then iterate over the results, returning these tuples, one on each next() call:

&mut[1,4,7], &mut[2,5,8], &mut[3,6,9]

I know that for general stuff it isn't possible to return mutable stuff, mut this is clearly disjoint, and without unsafe code we can have the ChunksMut (https://doc.rust-lang.org/std/slice/struct.ChunksMut.html) iterator, so maybe there's a way!. For example, I can have 3 ChunksMut and then the compiler knows that the elements returned from them are disjoint.

This is my try for non mutable:

https://play.rust-lang.org/?version=stable&mode=debug&edition=2021&gist=cfa7ca0bacbe6f1535050cd7dd5c537c

PS: I want to avoid Vec or any allocation on each iteration

Rafaelo
  • 33
  • 1
  • 15
  • https://play.rust-lang.org/?version=stable&mode=debug&edition=2021&gist=1b2f7401f724928ca2ad61075c7592fc but really just call `slice.chunks(chunk_size)` directly no need for such shortcut – Stargateur Nov 17 '21 at 00:38
  • @Stargateur, I think the output that the OP expected is &[1,4,7], &[2,5,8], &[3,6,9], instead of &[1,2,3], &[4,5,6], &[7,8,9]. – Joe_Jingyu Nov 17 '21 at 01:00
  • oh... well... well... impossible to do what op want without GaTs – Stargateur Nov 17 '21 at 01:14
  • 1
    Does this answer your question? [How do I write an iterator that returns references to itself?](https://stackoverflow.com/questions/30422177/how-do-i-write-an-iterator-that-returns-references-to-itself) – Stargateur Nov 17 '21 at 01:15
  • best I can give you without nightly https://play.rust-lang.org/?version=stable&mode=debug&edition=2021&gist=e461ea07a4774b995353eb91deddc09d – Stargateur Nov 17 '21 at 01:37
  • 5
    I think it might make sense to give us a bit more surrounding context. Do you really require your iterator to return elements of type `&[T]` (i.e. slices)? If so, you probably won't get around rearranging the elements (or packing them into a `Vec`) so that they lie contiguously in memory. If not, there may be a better alternative (e.g. using a combination of `skip` and `step_by`). Is the input to your iterator always a slice? Could you possibly reorder that slice so that after reordering you can simply use `chunks`? – phimuemue Nov 17 '21 at 13:33
  • @Rafaelo It is not possible to get a mutable view using a shared reference without invoking undefined behaviour. If you do not have a mutable reference to the original array/slice or the ownership of it; you can't achieve what you want without copying all the elements to a new memory, though this memory doesn't have to be on the heap. Also note that the [`slice.chunks_mut`](https://doc.rust-lang.org/std/primitive.slice.html#method.chunks_mut) method requires a mutable reference to the slice but you have a shared reference: `&[1,2,3,4,5,6,7,8,9]`. – Ekrem Dinçel Nov 26 '21 at 15:59

2 Answers2

4

so I always return a reference to its internal slice

The Iterator trait doesn't support this, because its contract allows the caller to extract several values and use all of them. For example, the following is permitted by Iterator but wouldn't be supported by your implementation:

// take two values out of the iterator
let a = it.next().unwrap();
let b = it.next().unwrap();

What you need is a "lending iterator" (also known as "streaming iterator"), see e.g. this crate. Writing lending iterators will become much easier once GATs are stabilized, but they still won't be supported by std::iter::Iterator.

Using the standard Iterator you can avoid allocation by using ArrayVec or equivalent replacement for Vec, as suggested by @Stargateur.

user4815162342
  • 141,790
  • 18
  • 296
  • 355
2

I'm pretty sure you wanted to get mutable references into the original slice using the iterator, resulting in &mut [&mut 1, &mut 4, &mut 7], &mut [&mut 2, &mut 5, &mut 8], &mut [&mut 3, &mut 6, &mut 9].

Without allocation / unsafe / external crates. Requires rust version 1.55 or greater:

fn iter_chunks<T, const CHUNK_SIZE: usize>(
    slice: &mut [T],
) -> impl Iterator<Item = [&mut T; CHUNK_SIZE]> + '_ {
    assert_eq!(slice.len() % CHUNK_SIZE, 0);
    let len = slice.len();
    let mut a: [_; CHUNK_SIZE] = array_collect(
        slice
            .chunks_mut(len / CHUNK_SIZE)
            .map(|iter| iter.iter_mut()),
    );
    (0..len / CHUNK_SIZE).map(move |_| array_collect(a.iter_mut().map(|i| i.next().unwrap())))
}

/// Builds an array from the first `N` items of an iterator
///
/// Panics:
///
/// If there are less then `N` items in the iterator
fn array_collect<T, const N: usize>(mut iter: impl Iterator<Item = T>) -> [T; N] {
    let a: [(); N] = [(); N];
    a.map(|_| iter.next().unwrap())
}

Without allocation, using an external crate:

We need to use arrayvec since Rust's array cannot be used with collect.

use arrayvec::ArrayVec;

fn main() {
    let slice = &mut [1, 2, 3, 4, 5, 6, 7, 8, 9];
    for (i, chunk) in iter_chunks::<_, 3>(slice).enumerate() {
        println!("{:?}", chunk);
        for t in chunk {
            *t = i;
        }
    }
    println!("slice: {:?}", slice);
}

fn iter_chunks<T, const CHUNK_SIZE: usize>(
    slice: &mut [T],
) -> impl Iterator<Item = ArrayVec<&mut T, CHUNK_SIZE>> + '_ {
    let len = slice.len();
    let mut a: ArrayVec<_, CHUNK_SIZE> = slice
        .chunks_mut(len / CHUNK_SIZE)
        .map(|chunk| chunk.iter_mut())
        .collect();
    (0..len / CHUNK_SIZE).map(move |_| {
        a.iter_mut()
            .map(|iter| iter.next().unwrap())
            .collect::<ArrayVec<_, CHUNK_SIZE>>()
    })
}

Output:

[1, 4, 7]
[2, 5, 8]
[3, 6, 9]
slice: [0, 1, 2, 0, 1, 2, 0, 1, 2]
susitsm
  • 465
  • 2
  • 6
  • Indeed, but this example uses allocation on every iteration, which I was trying to avoid – Rafaelo Nov 25 '21 at 11:12
  • then just save all the mutable references to 1 vector, reorder them, and use `chunks_mut` on it. – susitsm Nov 25 '21 at 11:17
  • Edited the answer to use 1 allocation only. If you want them to be slices, you need at least 1 allocation. If you just need an iterator with that ordering, I'm not sure. – susitsm Nov 25 '21 at 11:31
  • unfortunately this is intended to be used for really large arrays so reallocating a large vector just to iterate over it would be really expensive – Rafaelo Nov 25 '21 at 11:39
  • An allocation of `chunk_size` is okay? – susitsm Nov 25 '21 at 11:50
  • yes, but if only once. chunk_size is 3 in this example but very likely never more than 20. I was trying without it but if needed then ok – Rafaelo Nov 25 '21 at 12:19
  • Edited it with no allocation – susitsm Nov 25 '21 at 16:23
  • Unfortunately delegating it to another crait is just delegating unsafe code for other people. I was trying to do it without external crates. I'm really sorry you've put effort into it – Rafaelo Nov 25 '21 at 17:11
  • 1
    @Rafaelo You should make it clear in the question that you want an answer without any unsafe and any external crates. Rust is a language with a minimal standard library, and that supports unsafe for good reasons, so neither constraint is self-evident. – user4815162342 Nov 29 '21 at 18:13
  • Added the no-unsafe no-alloc no-dependency version – susitsm Dec 02 '21 at 05:48