[pre-RFC] TryFromIterator and try_collect to enable collecting to arrays

I was experimenting with const generics and found out that it is not possible to collect an iterator into an array, which is very annoying, when you are working in a no-std environment.

This is my first time writing a (pre-)RFC, so any feedback is greatly appreciated :upside_down_face:

(especially spelling and grammar errors)


Summary

The standard library provides the FromIterator trait, which can be used to construct types from an Iterator, however the construction can not fail. This RFC proposes the addition of the TryFromIterator trait to support these use cases in a standard way.

Motivation

Right now it is not possible to collect an Iterator into a fixed size array.

With the stabilization of const-generics fixed size arrays are used in a lot more places. A lot of times the size of an Iterator is already known, so one could collect into an array instead of a Vec, which requires heap allocations.

For example this iterator:

[1, 2, 3, 4].iter().map(i32::to_string);

only maps its items, so the size does not change, and it is already known beforehand that the size is 4.

Another example is the following:

use core::array;

fn convert_array<T, const N: usize>(array: [Option<T>; N]) -> Option<[T; N]> {
    array::IntoIter::new(array)
        // remove all None items from the iterator
        .flat_map(Option::into_iter)
        // try to collect into the array
        .try_collect::<[T; N]>()
        // if one of the items was None, the array is too large
        // -> collecting fails and None is returned
        .ok()
}

assert_eq!(convert_array([Some("1"), Some("2"), Some("3")]), Some(["1", "2", "3"]));
assert_eq!(convert_array([Some("1"), None, Some("3")]), None);
assert_eq!(convert_array([Some("1"), None, None]), None);

which maps an array ([T; N]) of Options to Option<[T; N]>.

use core::str::pattern::Pattern;

pub trait StrExt {
    fn split_at_most<'a, P, const N: usize>(&'a self, pat: P) -> [Option<&'a str>; N]
    where
        P: Pattern<'a>;
}

impl StrExt for str {
    fn split_at_most<'a, P, const N: usize>(&'a self, pat: P) -> [Option<&'a str>; N]
    where
        P: Pattern<'a>
    {
        // NOTE: can be unwrapped, because the error is ! (see below)
        self.splitn(N, pat).try_collect().unwrap()
    }
}

let string = "255 20 100";

if let [Some(red), Some(green), Some(blue)] = string.split_at_most::<_, 3>(' ') {
    println!("red: {}, green: {}, blue: {}", red, green, blue);
} else {
    println!("error invalid string");
}

Detailed design

A new trait will be added to the core::iter module:

trait TryFromIterator<A>: Sized {
    type Error;

    fn try_from_iter<T: IntoIterator<Item = A>>(iter: T) -> Result<Self, Self::Error>;
}

It is similar to TryFrom and From, but for FromIterator.

The Iterator trait will be extended with the following method:

trait Iterator {
    type Item;

    // ...

    fn try_collect<B: TryFromIterator<Self::Item>>(self) -> Result<B, B::Error>
    where
        Self: Sized,
    {
        TryFromIterator::try_from_iter(self)
    }
}

All types that implement FromIterator should implement TryFromIterator (similar to all types that implement From implement TryFrom)

use core::iter::{FromIterator, IntoIterator};

impl<A, B> TryFromIterator<A> for B
where
    B: FromIterator<A>,
{
    type Error = !;

    fn try_from_iter<T: IntoIterator<Item = A>>(iter: T) -> Result<Self, Self::Error> {
        Ok(<Self as FromIterator<T>>::from_iter(iter))
    }
}

The following implementation will be added for arrays:

use core::mem::{self, MaybeUninit};

#[derive(Debug, Clone, PartialEq, Eq, PartialOrd, Ord, Hash)]
enum CollectArrayError {
    NotEnoughItems { missing: usize },
}

impl<A, const N: usize> TryFromIterator<A> for [A; N] {
    type Error = CollectArrayError;

    fn try_from_iter<T: IntoIterator<Item = A>>(iter: T) -> Result<Self, Self::Error> {
        let mut array: [MaybeUninit<A>; N] = MaybeUninit::uninit_array();
        let mut iterator = iter.into_iter();

        for (i, item) in array.iter_mut().enumerate() {
            if let Some(value) = iterator.next() {
                *item = MaybeUninit::new(value);
            } else {
                return Err(CollectArrayError::NotEnoughItems { missing: N - i });
            }
        }

        // One can not simply use mem::transmute, because of this issue:
        // https://github.com/rust-lang/rust/issues/61956
        let result: [A; N] = unsafe {
            // assert that we have exclusive ownership of the array
            let pointer: *mut [A; N] = &mut array as *mut _ as *mut [A; N];
            let result: [A; N] = pointer.read();
            // forget about the old array
            mem::forget(array);

            result
        };

        Ok(result)
    }
}

Drawbacks

Rationale and alternatives

  • FromIterator implementation for arrays that panics (problematic, because one might want to handle the error case, see the convert_array example in Motivation)

  • One could collect into a structure, instead of adding a new trait:

use core::array;
use core::convert::TryInto;
use core::iter::FromIterator;
use core::mem::{self, MaybeUninit};

#[derive(Debug, Clone, PartialEq, Eq, PartialOrd, Ord, Hash)]
enum CollectArrayError {
    NotEnoughItems { missing: usize },
}

#[derive(Debug, Clone, PartialEq)]
struct ArrayCollector<T, const N: usize>(Result<[T; N], CollectArrayError>);

impl<A, const N: usize> FromIterator<A> for ArrayCollector<A, N> {
    fn from_iter<I: IntoIterator<Item = A>>(iter: I) -> Self {
        let mut array: [MaybeUninit<A>; N] = MaybeUninit::uninit_array();
        let mut iterator = iter.into_iter();

        for (i, item) in array.iter_mut().enumerate() {
            if let Some(value) = iterator.next() {
                *item = MaybeUninit::new(value);
            } else {
                return Self(Err(CollectArrayError::NotEnoughItems { missing: N - i }));
            }
        }

        // One can not simply use mem::transmute, because of this issue:
        // https://github.com/rust-lang/rust/issues/61956
        let result: [A; N] = unsafe {
            // assert that we have exclusive ownership of the array
            let pointer: *mut [A; N] = &mut array as *mut _ as *mut [A; N];
            let result: [A; N] = pointer.read();
            // forget about the old array
            mem::forget(array);

            result
        };

        Self(Ok(result))
    }
}

impl<T, const N: usize> TryInto<[T; N]> for ArrayCollector<T, N> {
    type Error = CollectArrayError;
    
    fn try_into(self) -> Result<[T; N], Self::Error> {
        self.0
    }
}

let iterator = array::IntoIter::new(["1", "2", "3", "4"]);

let result: Result<[_; 4], _> = iterator.collect::<ArrayCollector<&str, 4>>()
                                        .try_into();

println!("result: {:?}", result);

The downside of this solution is that it is quite verbose to use and especially beginners could struggle with it (uses many traits, indirections and type annotations).

Another problem is that it does not allow future extensions with different types (for example one might want to collect into homogeneous tuples).

  • The trait could look like this:
trait TryFromIterator<A>: Sized {
    type Error;
    type StrictError;

    fn try_from_iter<T: IntoIterator<Item = A>>(iter: T) -> Result<(Self, T::IntoIter), Self::Error>;

    fn try_from_iter_strict<T: IntoIterator<Item = A>>(iter: T) -> Result<Self, Self::StrictError>;
}

where the try_from_iter function returns the Iterator, with the remaining elements and try_from_iter_strict would error if there are remaining elements in the Iterator.

The downside of this solution is that it is unnecessarily complicated. The same can be achieved with the proposed solution, by passing a mutable reference to the try_from_iter function. (there exists an implementation of Iterator for all mutable references to Iterator)

use core::array;

let mut iterator = array::IntoIter::new([1, 2, 3]);

let array: [_; 2] = iterator.by_ref().try_collect()?;

assert_eq!(array, [1, 2]);
assert_eq!(iterator.next(), Some(3));
assert_eq!(iterator.next(), None);
  • One could add a blanke impl of FromIterator for Resutl<[T; N], CollectArrayError> instead:
// one can not implement foreign traits on foreign types
#[derive(Clone, PartialEq, Debug)]
pub enum CustomResult<T, E> {
    Ok(T),
    Err(E),
}

impl<A, const N: usize> FromIterator<A> for CustomResult<[A; N], CollectArrayError> {
    fn from_iter<T: IntoIterator<Item = A>>(iter: T) -> Self {
        let mut array: [MaybeUninit<A>; N] = MaybeUninit::uninit_array();
        let mut iterator = iter.into_iter();

        for (i, item) in array.iter_mut().enumerate() {
            if let Some(value) = iterator.next() {
                *item = MaybeUninit::new(value);
            } else {
                return Self::Err(CollectArrayError::NotEnoughItems { missing: N - i });
            }
        }

        let result: [A; N] = unsafe {
            // assert that we have exclusive ownership of the array
            let pointer: *mut [A; N] = &mut array as *mut _ as *mut [A; N];
            let result: [A; N] = pointer.read();
            // forget about the old array
            mem::forget(array);

            result
        };

        Self::Ok(result)
    }
}

let values: [usize; 6] = [1, 2, 3, 4, 5, 6];
let result = values.iter().map(|v| v + 1).collect::<CustomResult<[usize; 6], CollectArrayError>>();

assert_eq!(CustomResult::Ok([2, 3, 4, 5, 6, 7]), result);

Downside is that it is too verbose, because you have to include the Result and the Error in the annotation.

Prior art

There exist several issues with different implementations:

Posts about this issue:

The arrayvec crate, which provides a fixed capacity ArrayVec, implements FromIterator and panics if there are too many elements in the iterator. (See here)

Unresolved questions

  • Implementation for Arrays contains questionable unsafe code (lots of testing would be required)
  • should try_from_iter fail if the array is too small?
  • Add the following implementation? this could be a FromIterator implementation?
impl<A, const N: usize> TryFromIterator<A> for [Option<A>; N] {
    type Error = !;

    fn try_from_iter<T: IntoIterator<Item = A>>(iter: T) -> Result<Self, Self::Error> {
        let mut array: [MaybeUninit<Option<A>>; N] = MaybeUninit::uninit_array();
        let mut iterator = iter.into_iter();

        for item in array.iter_mut() {
            *item = MaybeUninit::new(iterator.next());
        }

        let result: [Option<A>; N] = unsafe {
            // assert that we have exclusive ownership of the array
            let pointer: *mut [Option<A>; N] = &mut array as *mut _ as *mut [Option<A>; N];
            let result: [Option<A>; N] = pointer.read();

            // forget about the old array
            mem::forget(array);

            result
        };

        Ok(result)
    }
}
impl<A> TryFromIterator<A> for (A, A, A) {
    type Error = CollectArrayError;

    fn try_from_iter<T: IntoIterator<Item = A>>(iter: T) -> Result<Self, Self::Error> {
        let mut iterator = iter.into_iter();
        Ok((
            iterator
                .next()
                .ok_or(CollectArrayError::NotEnoughItems { missing: 3 })?,
            iterator
                .next()
                .ok_or(CollectArrayError::NotEnoughItems { missing: 2 })?,
            iterator
                .next()
                .ok_or(CollectArrayError::NotEnoughItems { missing: 1 })?,
        ))
    }
}
  • because CollectArrayError has only one variant, make it a struct and call it NotEnoughItemsError?

Future possibilities

In the future one might implement this trait for homogeneous tuples of any size:

if let (a, b, c) = "some example string".split(' ').try_collect::<(&str, &str, &str)>() {
    println!("a: {}, b: {}, c: {}", a, b, c);
}

or one could do the following:

// NOTE: syntax might change
// fn split_at_most<'a, P, const N: usize>(&'a self, pat: P) -> (&str, ..Option<&str>)
// where
//     P: Pattern<'a>;
if let (a, Some(b), Some(c)) = "some example string".split_at_most::<_, 3>(' ') {
    println!("a: {}, b: {}, c: {}", a, b, c);
}

Seems better to collect() into an ad-hoc structure like this:

struct ArrayCollect<T, const n: usize> {
    array: ArrayVec<T, n>,
    more_items: bool
}

And then provide helpers and TryFrom impls to convert into an array or arrayvec.

This avoids having to add any new API.

2 Likes

Note that we already have a planned method for it:

https://doc.rust-lang.org/std/primitive.array.html#method.map

2 Likes

I was wondering why should too many items be an error? And also CollectArrayError is more rustic than ArrayCollectError (see std::num::ParseIntError - Rust not IntParseError)

1 Like

Would something like iter_fixed be relevant to this discussion? It is some types and traits for doing iterator-like things to things of fixed size. Exampe:

let res: [_; 2] = [1, 2, 3, 4]
    .into_iter_fixed()
    .zip([4, 3, 2, 1])
    .map(|(a, b)| a + b)
    .skip::<1>()
    .take::<2>()
    .collect();

Edit: The big thing here is that there is no need to check the length at runtime. So no try_collect/panicking collect needed.

3 Likes

I generally like this, however I do have some comments.

  1. Your motivating example could be better. Since String requires heap allocations this example could be better (since in your first paragraph you mention no_std).

  2. I think that it shouldn't be an error case to have more than enough elements. I think that a more general (and more powerful API) would be to have the trait be the following:

    trait TryFromIterator<A>: Sized {
        type Error;
        type StrictError;
    
        fn try_from_iter<T: IntoIterator<Item = A>>(iter: T) -> Result<(Self, T), Self::Error>;
    
        fn try_from_iter_strict<T: IntoIterator<Item = A>>(iter: T) -> Result<Self, Self::StrictError>;
    }
    

    This way you don't throw away the work of collecting if there are more items, in the try_from_iter case, but you can opt into that when using try_from_iter_strict.

  3. Your example with Option<A> I think is wrong since you are de referencing a mutable reference to a MaybeUninit<>. Which I think is instant-UB (though I might be wrong). I thought that you had to write to them using raw pointer.

  4. try_from_iter (for arrays) would have to fail if the array is too small. Unless the error case passed back to the user the ([MaybeUninit<A>; N], usize) so that the user could take out the items themselves, but that seems like a lot of work for an initial implementation.

Those are just my initial thoughts. Good work on your first Pre-RFC.

3 Likes

Could we have blanket impls of FromIterator for Result<[T; N], ArrayFromIterError> instead?

3 Likes

This was mentioned in the "downsides" section:

That's not possible. Under the current orphan rules, an user can write an impl like

impl<E, const N: usize> iter::FromIterator<LocalStruct> for [Result<LocalStruct, E>; N]

which together with the already stable

impl<A, E, V: FromIterator<A>> FromIterator<Result<A, E>> for Result<V, E>

would produce an implementation of FromIterator<Result<LocalStruct, E>> for Result<[Result<LocalStruct, E>; N], E>. Replacing E with ArrayFromIterError would produce an impl that overlaps with yours, thus your impl would be a breaking change. In fact the compiler rejects it:

error[E0119]: conflicting implementations of trait `iter::traits::collect::FromIterator<result::Result<_, array::iter::ArrayFromIterError>>` for type `result::Result<[result::Result<_, array::iter::ArrayFromIterError>; _], array::iter::ArrayFromIterError>`:
    --> library\core\src\result.rs:1577:1
     |
1577 | impl<A, E, V: FromIterator<A>> FromIterator<Result<A, E>> for Result<V, E> {
     | ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ conflicting implementation for `result::Result<[result::Result<_, array::iter::ArrayFromIterError>; _], array::iter::ArrayFromIterError>`
     | 
    ::: library\core\src\array\iter.rs:240:1
     |
240  | impl<T, const N: usize> iter::FromIterator<T> for Result<[T; N], ArrayFromIterError> {
     | ------------------------------------------------------------------------------------ first implementation here
     |
     = note: downstream crates may implement trait `iter::traits::collect::FromIterator<_>` for type `[result::Result<_, array::iter::ArrayFromIterError>; _]`

I thought OP meant that users could be confused between Result's FromIterator and TryFromIterator implementations.

2 Likes

I do not think that this would be a very good solution, maybe you could provide a full example?

I had another idea for a blanket implementation thanks to your post, but it does not seem to work:

use core::convert::TryFrom;

impl<A, B, I> TryFromIterator<A> for B
where
    B: TryFrom<I>,
    I: IntoIterator<Item = A>,
{
    type Error = <B as TryFrom<I>>::Error;

    fn try_from_iter<T: IntoIterator<Item = A>>(iter: T) -> Result<Self, Self::Error> {
        TryFrom::try_from(iter)
    }
}

Very cool, I did not know that this method exists, but it only works with map (I will update my post with a more complicated example :upside_down_face:)

Yes there should be a better example, maybe the convert_array example from below?

I am not really sure if it should be an error. I think it could be problematic with infinite iterators, but if you have an infinite iterator you could do:

use core::iter;

let array = iter::repeat("a").take(10).try_collect::<[&'static str; 10]>().unwrap();

I think CollectArrayError is a bit better, so I will update my post.

I think the following would not be possible with the linked crate (or maybe I am not smart enough?):

fn convert_array<T, const N: usize>(array: [Option<T>; N]) -> Option<[T; N]> {
    if let Ok(result) = core::array::IntoIter::new(array).flat_map(Option::into_iter).try_collect::<[T; N]>() {
        Some(result)
    } else {
        None
    }
}

assert_eq!(convert_array([Some("1"), Some("2"), Some("3")]), Some(["1", "2", "3"]));
assert_eq!(convert_array([Some("1"), None, Some("3")]), None);
assert_eq!(convert_array([Some("1"), None, None]), None);

This seems to be a better API, but it is also more complicated (I need some time to think about it)

Do you know, how to implement this, without undefined behavior?

Yes this is possible:

// one can not implement foreign traits on foreign types
#[derive(Clone, PartialEq, Debug)]
pub enum CustomResult<T, E> {
    Ok(T),
    Err(E),
}

impl<A, const N: usize> FromIterator<A> for CustomResult<[A; N], CollectArrayError> {
    fn from_iter<T: IntoIterator<Item = A>>(iter: T) -> Self {
        let mut array: [MaybeUninit<A>; N] = MaybeUninit::uninit_array();
        let mut iterator = iter.into_iter();

        for (i, item) in array.iter_mut().enumerate() {
            if let Some(value) = iterator.next() {
                *item = MaybeUninit::new(value);
            } else {
                return Self::Err(CollectArrayError::NotEnoughItems { missing: N - i });
            }
        }

        let result: [A; N] = unsafe {
            // assert that we have exclusive ownership of the array
            let pointer: *mut [A; N] = &mut array as *mut _ as *mut [A; N];
            let result: [A; N] = pointer.read();
            // forget about the old array
            mem::forget(array);

            result
        };

        Self::Ok(result)
    }
}

let values: [usize; 6] = [1, 2, 3, 4, 5, 6];
let result = values.iter().map(|v| v + 1).collect::<CustomResult<[usize; 6], CollectArrayError>>();

assert_eq!(CustomResult::Ok([2, 3, 4, 5, 6, 7]), result);

Note that you have to also include the Result and the error in the annotation, which could be confusing for beginners and this is not very beautiful.

Yes this is what I meant.

Here is a fully working example, based on your response:

use core::mem::{self, MaybeUninit};

trait TryFromIterator<A>: Sized {
    type Error;
    type StrictError;

    fn try_from_iter<T: IntoIterator<Item = A>>(iter: T) -> Result<(Self, T::IntoIter), Self::Error>;

    fn try_from_iter_strict<T: IntoIterator<Item = A>>(iter: T) -> Result<Self, Self::StrictError>;
}

#[derive(Debug, Clone, PartialEq)]
pub enum CollectArrayError {
    NotEnoughItems { missing: usize },
}

#[derive(Debug, Clone, PartialEq)]
pub enum CollectArrayStrictError {
    NotEnoughItems { missing: usize },
    TooManyItems { remaining: usize },
}

impl From<CollectArrayError> for CollectArrayStrictError {
    fn from(value: CollectArrayError) -> Self {
        match value {
            CollectArrayError::NotEnoughItems { missing } => Self::NotEnoughItems { missing }
        }
    }
}

impl<A, const N: usize> TryFromIterator<A> for [A; N] {
    type Error = CollectArrayError;
    type StrictError = CollectArrayStrictError;

    fn try_from_iter<T: IntoIterator<Item = A>>(iter: T) -> Result<(Self, T::IntoIter), Self::Error> {
        let mut array: [MaybeUninit<A>; N] = MaybeUninit::uninit_array();
        let mut iterator = iter.into_iter();

        for (i, item) in array.iter_mut().enumerate() {
            if let Some(value) = iterator.next() {
                *item = MaybeUninit::new(value);
            } else {
                return Err(CollectArrayError::NotEnoughItems { missing: N - i });
            }
        }

        let result: [A; N] = unsafe {
            // assert that we have exclusive ownership of the array
            let pointer: *mut [A; N] = (&mut array as *mut [MaybeUninit<A>; N]).cast::<[A; N]>();
            let result: [A; N] = pointer.read();
            // forget about the old array
            mem::forget(array);

            result
        };

        Ok((result, iterator))
    }

    fn try_from_iter_strict<T: IntoIterator<Item = A>>(iter: T) -> Result<Self, Self::StrictError> {
        let (result, mut iterator) = <Self as TryFromIterator<A>>::try_from_iter(iter)?;

        if iterator.next().is_some() {
            return Err(CollectArrayStrictError::TooManyItems { remaining: iterator.count() + 1 });
        }

        Ok(result)
    }
}

I like this a lot more, the only thing that I am not happy with are the errors (two separate enums and one of them has only one variant)

Maybe it would be possible to provide a blanket implementation for try_from_iter_strict? (change to type StrictError: From<Self::Error>)

Possible? - Yes... Safe and ergonomic? - No, not really

use iter_fixed::IteratorFixed;
use std::array;

fn convert_array<T, const N: usize>(array: [Option<T>; N]) -> Option<[T; N]> {
    if array.iter().all(|o| o.is_some()) {
        let iter = array::IntoIter::new(array).flat_map(Option::into_iter);
        
        // Safety: We have just checked that all elements of `array` are Some thus the
        // iterator above will have length N
        let iter_fixed = unsafe {
            IteratorFixed::from_iter(iter)
        };
        Some(iter_fixed.collect())
    } else {
        None
    }
}

So - No, something like iter_fixed probably wont help much for iterators of non-known length.

1 Like

What about implementing the trait for tuples?

An implementation could look like this (note: no unsafe code):

impl<A> TryFromIterator<A> for (A, A, A) {
    type Error = CollectArrayError;

    fn try_from_iter<T: IntoIterator<Item = A>>(iter: T) -> Result<Self, Self::Error> {
        let mut iterator = iter.into_iter();
        Ok((
            iterator
                .next()
                .ok_or(CollectArrayError::NotEnoughItems { missing: 3 })?,
            iterator
                .next()
                .ok_or(CollectArrayError::NotEnoughItems { missing: 2 })?,
            iterator
                .next()
                .ok_or(CollectArrayError::NotEnoughItems { missing: 1 })?,
        ))
    }
}

with macros, the trait could be implemented for multiple tuples:

// taken from: https://danielkeep.github.io/tlborm/book/blk-counting.html
macro_rules! count_tts {
    () => {0usize};
    ($_head:tt $($tail:tt)*) => {1usize + count_tts!($($tail)*)};
}

macro_rules! tuple_impls {
    ($(
        $Tuple:ident {
            $(($idx:tt) -> $T:ident)+
        }
    )+) => {
        $(
            impl<A> TryFromIterator<A> for ($($T),+) {
                type Error = CollectArrayError;

                fn try_from_iter<T: IntoIterator<Item = A>>(iter: T) -> Result<Self, Self::Error> {
                    let mut iterator = iter.into_iter();
                    let total = count_tts!( $($T )+ );
                    Ok((
                        $(iterator.next().ok_or(CollectArrayError::NotEnoughItems { missing: total - $idx })?),+
                    ))
                }
            }
        )+
    };
}

tuple_impls! {
    Tuple1 {
        (0) -> A
    }
    Tuple2 {
        (0) -> A
        (1) -> A
    }
    Tuple3 {
        (0) -> A
        (1) -> A
        (2) -> A
    }
    Tuple4 {
        (0) -> A
        (1) -> A
        (2) -> A
        (3) -> A
    }
    
    // ... up to 12
}

I am not sure if there are many uses for this (an array is a homogeneous tuple?), so one might wait with implementing this, until variable sized tuples are implemented.

It's definitely possible (at least on nightly) to implement regular FromIterator for fixed-sized arrays (or more broadly structs built directly around them using const generics). You just have to kind of stop collecting when the array reaches maximum capacity. I've done this in several ways in a crate of mine.

I've just published https://crates.io/crates/collect_array - would love some thoughts/PRs if it's useful (or not!) to anyone :slight_smile:

I feel like an interesting (albeit slightly undiscoverable and maybe a bit implementation-details-leaky) but potentially useful place for this API could be on std::iter::Peekable - Rust. It feels a little weird as a location, but having the API be:

impl <I: Iterator> Peekable<I> {
    pub fn collect_to_array<const N: usize>(self) -> CollectArrayResult<I::Item, I, N>;
}

pub enum CollectArrayResult<T, I: Iterator<Item = T>, const N: usize> {
    Ok([T; N]),
    TooManyElements {
        values: [T; N],
        remaining: Peekable<I>,
    },
    NotEnoughElements {
        values: [MaybeUninit<T>; N],
        init_count: usize,
    },
}

allows for checking for too many elements easily, and having a nice way to return "the rest of the (potentially infinite) Iterator" back to the caller... I think this is much harder to do in a general trait because of object safety, but really easy to do on Peekable...

2 Likes

More prior art: arrayvec::ArrayVec - Rust

I think collecting to such a type, rather than an array directly, is a perfectly-reasonable way to do this.

1 Like

Error handling changed from silent truncation to panic on extra elements in arrayvec 0.6 or later. :slightly_smiling_face:

Yeah, I went back to find the old version :wink:

Updated the post with arrayvec listed under prior art and added more examples