Recently I needed to take two arrays of the same size and generate another array by mapping their elements in order.
My first instinct was to reach for zip method to create an array of tuples and then map that array, like this:
let a = [5, 4, 7, 9];
let b = [3, 2, 6, 1];
let c = a.zip(b).map(|(a, b)| a - b);
But to my surprise, there is no zip method on arrays, and I was forced to use std::array::from_fn for a clunky solution, like this:
...
let c: [_; 4] = std::array::from_fn(|i| a[i] - b[i]);
Is there any reason why a zip method is not implemented for two arrays of the same length?
Most recent relevant change I found: it existed but was removed.
[T; N]::zip is "eager" but most zips are mapped. This causes poor optimization in generated code. This is a fundamental design issue and "zip" is "prime real estate" in terms of function names, so let's free it up again.
I think some form of zip_with might be more plausible. In an eager API, an array of pairs is really not what anyone actually wanted, but if the zip+map were one call -- rather than something in a bad order because of the eagerness -- then it might be ok.
But ideally someone can come up with a clever transformer API of some sort so you can write the transformation as one thing, then apply it to a set of inputs to get what you need, so that like Iterators it composes well and doesn't make unfortunate eager intermediaries. No idea what that would look like, though...
My power's out due to (outer parts of) Hurricane Helene and I felt like it, so Writing a version of array_map yourself isn't too bad, e.g.
use std::{ptr, mem::{MaybeUninit, ManuallyDrop}};
// use C style loop for stable const
pub fn array_zip_const<T, U, const N: usize>(ts: [T; N], us: [U; N]) -> [(T, U); N] {
let ts = ManuallyDrop::new(ts);
let us = ManuallyDrop::new(us);
let mut zip = MaybeUninit::<[(T, U); N]>::uninit();
for i in 0..N {
// SAFETY: i < N
let (t_i, u_i, out) = unsafe {(
&raw const ts[i],
&raw const us[i],
&raw mut (*zip.as_mut_ptr())[i],
)};
// SAFETY: ts[i] moved from once, zip[i].0 moved to once
unsafe { ptr::copy_nonoverlapping(t_i, &raw mut (*out).0, 1) };
// SAFETY: us[i] moved from once, zip[i].1 moved to once
unsafe { ptr::copy_nonoverlapping(u_i, &raw mut (*out).1, 1) };
}
// SAFETY: zip has been fully initialized
unsafe { zip.assume_init() }
}
// safer but nonconst due to <[_]>::map
pub fn array_zip<T, U, const N: usize>(ts: [T; N], us: [U; N]) -> [(T, U); N] {
let mut ts = ts.map(ManuallyDrop::new);
let mut us = us.map(ManuallyDrop::new);
let mut zip = [const { MaybeUninit::<(T, U)>::uninit() }; N];
for i in 0..N {
// SAFETY: ts[i] taken once, untouched afterwards
let t = unsafe { ManuallyDrop::take(&mut ts[i]) };
// SAFETY: us[i] taken once, untouched afterwards
let u = unsafe { ManuallyDrop::take(&mut us[i]) };
zip[i].write((t, u));
}
// SAFETY: zip has been fully initialized
unsafe { MaybeUninit::array_assume_init(zip) }
}
…although this is entirely untested since the playground seemed to be down so don't trust it.
(EDIT: fixed silly type errors)
But I concur that this is a stack-copy over-reliance footgun in practice. A theoretical IntoConstSizeIterator would likely be a nicer way to handle "iterator like" methods for arrays.
the outage must have just ended, it seems to be working now. Throughout, (at least for me), play.integer32.com seems to have kept working btw, that’s a reasonable alternative to test in cases like this (they even use the same GitHub Gists account, so you can just re-brand a share link back to rust-lang.org and it’ll work).