The Problem
I was experimenting with const generics and I noticed a pattern that cropped up quite a bit. I was using a wrapper around a [T; N]
called Vector to do some linear algebra:
struct Vector<T, const N: usize>([T; N])
I wanted to implement elementwise addition and subtraction to this struct.
let a = Vector([1, 2, 3)];
let b = Vector([1, 1, 1]);
assert_eq(a + b, Vector([2, 3, 4]));
In order to implement elementwise addition with safe rust I used this impl:
impl<T: Add<Output = T> + Copy + Default, const N: usize> Add<T> for Vector<T, { N }> {
type Output = Self;
fn add(self, other: T) -> Vector<T, N> {
let mut out: [T; N] = [Default::default(); N];
for x in 0..N {
out[x] = self.0[x] + other;
}
Self(out)
}
}
There is a problem here that could be fixed, the Default
impl is technically unnecessary and simply used to create an initialized array. With unsafe code it is possible to remove the Default
impl.
impl<T: Add<Output = T> + Copy, const N: usize> Add for Vector<T, { N }> {
type Output = Self;
fn add(self, other: Vector<T, N>) -> Vector<T, N> {
let mut out: [MaybeUninit<T>; N] = unsafe { MaybeUninit::uninit().assume_init()};
for x in 0..N {
out[x] = MaybeUninit::new(self.0[x] + other.0[x]);
}
let arr = unsafe { mem::transmute_copy::<[MaybeUninit<T>; N],[T; N]>(&out) };
Self(arr)
}
}
The transmute_copy
is due to transmute currently not working with const generics. I don't write much unsafe code, so I'm not sure about this safe.
More generally, there would be quite an obvious desire to make it ergonomic to write functions with a signature like this:
fn example<const N: usize>() -> [usize; N]
where there would be some elementwise operation occuring inside the function.
How can we improve this
(1) Allow initially uninitialed arrays (but initialized before returning) to compile.
Now the cleanest way of allowing these kinds of functions to be implemented is to simply allow a function like this compile:
fn add<const N: usize>(a: [usize; N], b: [usize; N]) -> [usize; N]{
let arr:[usize; N];
for idx in 0..N {
arr[idx] = a[idx] + b[idx];
}
arr
}
Currently this does not compile due to arr possibly not being initialized. This seems like it would be a lot of work to implement in the compiler. A slight alteration would be to create a special syntax for doing this kind of operation.
(2) Trait level support for collecting on a array.
Iterators already are an abstraction over performing elementwise operations. Therefore another option would be to provide some trait level for producing an array from support to abstract the idea of "collecting" into an array from an iterator. This could be a FromIterator
impl (although this feels like a bit of an abuse of the trait.) Another option would be to add a new trait (maybe ConstFromIterator
?) to abstract over converting an iterator to an array. We could use ExactSizeIterator
to aid in this.
This could allow the same interface to be used with other const-generic collections (such as collecting into my Vector.)
For example, the addition of the FromIterator
trait would make the my Add impl look like this:
impl<T: Add<Output = T> + Copy, const N: usize> Add for Vector<T, { N }> {
type Output = Self;
fn add(self, other: Vector<T, N>) -> Vector<T, N> {
self.0
.iter()
.zip(other.0.iter())
.map(|(a, b)| *a + *b)
.collect()
}
}
Notice that there is no unsafe and no Default
impl. it also looks a lot cleaner. A demo of the ForIterator
impl would be:
impl<T, const N: usize> FromIterator<T> for Vector<T, N> {
fn from_iter<I>(iter: I) -> Self
where
I: IntoIterator<Item = T>,
{
let mut out: [MaybeUninit<T>; N] = unsafe { MaybeUninit::uninit().assume_init() };
let mut iter = iter.into_iter();
for idx in 0..N {
match iter.next() {
Some(x) => out[idx] = MaybeUninit::new(x),
None => panic!(),
}
}
unsafe { mem::transmute_copy::<[MaybeUninit<T>; N], [T; N]>(&out) }
}
}
This is horrendously unsafe (again) but it shows that the impl shields the library author from unsafe.
(3) Free function/macro/crate support
Free function or macro support. The third option is not to require any trait and instead use a free function to convert an iterator to an array, perhaps a function with the signature:
fn const_collect<I, T, const N: usize>(iter: I) -> [T; N]
where
I: IntoIterator<Item = T>,
T: Copy
This would essentially do the same as the FromIterator
impl.
Conclusion
The option of a new trait and a free function/macro do not absolutlely require std support so it may not be a libs/lang issue, but it feels like there should be std/lang support for this eventually.
It might be possible to do this in safe code by iterating on a slice, then using the TryFrom<&'a [T]> for [T; N]
impl. Even if this is safe this is quite ugly.
Obviously this is far down the line from a const-generics MVP, but I thought it would be cool to think about how const-generics could actually be used and make them ergonomic.