Repository navigation
Plan for dimension types #519
Copy link
Copy link
Open
Description
I've been thinking about the dimension types for a while. Two things I don't like about the current implementation are that:
- Strides are represented as
usizeand have to be converted toisizeevery time they're used. This is error-prone and confusing. - It seems a little weird that the dimension type itself (instead of an associated type) has a representation. For example, "2-D" doesn't necessary imply a specific representation to me.
What I'd like to do is something like the following:
pub trait Dimension
where
for<'a> Into<Self::OwnedUsize> for &'a Self::BorrowedUsize,
for<'a> Into<Self::OwnedIsize> for &'a Self::BorrowedIsize,
{
type OwnedUsize: AsRef<Self::BorrowedUsize> + AsMut<Self::BorrowedUsize> + AsRef<[usize]> + AsMut<[usize]>;
type BorrowedUsize: ?Sized + AsRef<[usize]> + AsMut<[usize]>;
type OwnedIsize: AsRef<Self::BorrowedIsize> + AsMut<Self::BorrowedIsize> + AsRef<[isize]> + AsMut<[isize]>;
type BorrowedIsize: ?Sized + AsRef<[isize]> + AsMut<[isize]>;
}
pub struct Ix2;
pub struct IxDyn;
impl Dimension for Ix2 {
type OwnedUsize = [usize; 2];
type BorrowedUsize = [usize; 2];
type OwnedIsize = [isize; 2];
type BorrowedIsize = [isize; 2];
}
impl Dimension for IxDyn {
type OwnedUsize = IxDynImpl<usize>;
type BorrowedUsize = [usize];
type OwnedIsize = IxDynImpl<isize>;
type BorrowedIsize = [isize];
}
pub trait IntoDimOwnedUsize {
type Dim: Dimension;
fn into_dim_owned_usize(self) -> Self::Dim::OwnedUsize;
}
pub trait AsDimBorrowedUsize {
type Dim: Dimension;
fn as_dim_borrowed_usize(&self) -> &Self::Dim::BorrowedUsize;
}
pub trait IntoDimOwnedIsize { ... }
pub trait AsDimBorrowedIsize { ... }
pub struct ArrayBase<S, D>
where
S: Data,
{
data: S,
ptr: *mut S::Elem,
dim: D::OwnedUsize,
strides: D::OwnedIsize,
}
impl<A, S, D> ArrayBase<S, D>
where
S: Data<Elem = A>,
D: Dimension,
{
pub fn shape(&self) -> &D::BorrowedUsize {
// ...
}
pub fn strides(&self) -> &D::BorrowedIsize {
// ...
}
}Once Rust has generic associated types, we can simplify this to:
pub trait Dimension
where
for<'a, T: Clone> Into<Self::Owned<T>> for &'a Self::Borrowed,
{
type Owned<T>: AsRef<Self::Borrowed<T>> + AsMut<Self::Borrowed<T>> + AsRef<[T]> + AsMut<[T]>;
type Borrowed<T>>: ?Sized + AsRef<[T]> + AsMut<[T]>;
}
pub struct Ix2;
pub struct IxDyn;
impl Dimension for Ix2 {
type Owned<T> = [T; 2];
type Borrowed<T> = [T; 2];
}
impl Dimension for IxDyn {
type Owned<T> = IxDynImpl<T>;
type Borrowed<T> = [T];
}
pub trait IntoDimOwned<T> {
type Dim: Dimension;
fn into_dim_owned(self) -> Self::Dim::Owned<T>;
}
pub trait AsDimBorrowed<T> {
type Dim: Dimension;
fn as_dim_borrowed(&self) -> &Self::Dim::Borrowed<T>;
}
pub struct ArrayBase<S, D>
where
S: Data,
{
data: S,
ptr: *mut S::Elem,
dim: D::Owned<usize>,
strides: D::Owned<isize>,
}
impl<A, S, D> ArrayBase<S, D>
where
S: Data<Elem = A>,
D: Dimension,
{
pub fn shape(&self) -> &D::Borrowed<usize> {
// ...
}
pub fn strides(&self) -> &D::Borrowed<isize> {
// ...
}
}I'd also add various type-level arithmetic operations on the dimension types, which are necessary for things like co-broadcasting (trait PartialOrdDim) and fold_axes (trait SubDim).
We can also add Shape<T>, Strides<T>, and Index<T> thin wrapper types.
This approach would resolve things like #489 and this comment on #367.
Thoughts?
Reactions are currently unavailable
Metadata
Metadata
Assignees
Labels
No labels
Activity
LukeMathWalker commentedon Oct 29, 2018
It looks cleaner, which is always good API-wise, but I have to admit that the whole dimension-shape situation is a little big foggy to me (especially since reading #367, I feel there is a lot I am missing there between the lines).
To get a proper understanding of what this implies could you frame it with respect to the other dimension-related traits?
bluss commentedon Nov 4, 2018
isize vs usize genericity is a low level problem. It would be nice to solve. Before those come other problems I think: possible division of a trait for indices vs a trait dimensions (current solution uses the same for both which is pragmatic but limiting), and like you point out, making the dimension-related traits more obvious and hopefully fewer in number.
LukeMathWalker commentedon Nov 18, 2018
I was rereading #367 and I think, even though there might some technical difficulties that will have to be ironed out, that what @termoshtt was proposing makes intuitive sense from an API point of view.
I would actually push it one step further, by further decomposing what we know call
Dimensioninto two things:NdimorDimensionalitytrait, used to check at compile time how many dimensions an array has. We could resort to something similar to what nalgebra does using typenum;Shapetrait, as inNumPy, providing information on how many elements there are in each axis of the array.Ndimwould be the only type, apart from element type, to appear in theArrayBasedeclaration, whileShapewill be inferred from the data passed to the constructor.A
MemoryLayoutwould then take the place of ourStrides-related existing functionalities.From a user point of view, I think this would result more "intuitive".
What are your thoughts?
jturner314 commentedon Nov 20, 2018
Yeah, I think it makes sense to separate the type that implements
Ndim/Dimensionalityfrom the representation of the shape/strides. Until Rust has generic associated types, I'm thinking about a simpler approach than I proposed earlier:This is similar to the existing implementation; the primary changes are:
Dimensionality) is separate from the representation of the shape/strides (the types that implementDimRepr).isizeinstead of casting to/fromusize.Everything else about the existing implementation stays pretty much the same, other than renaming some things.
I've chosen to expose the representation of fixed-dimension shape/strides as fixed-length arrays, but that's not strictly necessary.
The remaining things I dislike about this are:
IntoShapecould be confusing for things other than shapes. (See.permuted_axes()for an example of this. IMO the nameIntoDimensionalityin this case is just as confusing asIntoShape. It would be nice to have anIntoAxestrait or something.)D: Dimensionalityto the corresponding type that implementsDimRepr<Elem = T>for arbitraryT, not justusize/isize. With currentndarrayand with this proposal, if you want to represent a collection ofndimitems that aren'tusize/isizegiven only theD: Dimensionality, you have to use aVec.bluss commentedon Nov 21, 2018
I was going to say first that in general:
I like your new approach jturner. It's good but doesn't seem complete, unsure if it solves enough problems.
I'll ask about a concrete problem in Array::zeros
This would be better with a bound like
Sh: IntoContiguousShape<Dim=D>, or anything that better evokes exactly what kind of argument is needed here.For array constructors we have three kinds of inputs:
For natural reasons we only allow (1) and (2) for most constructors, and (3) for some advanced constructors, and this is the reason we use the type system to only accept (3) where we can support it. Current bounds accomplish this, but the traits and trait names don't make it very clear.
For me, "Layout" means something like C/F memory layout, it doesn't mean the lengths of each axis, so I'm unsure about the name.
LukeMathWalker commentedon Nov 24, 2018
I like @jturner314's design - wouldn't it be better though to use two separate traits for Shapes and Strides? (even if they might offer exactly the same methods initially)
Currently we are constraining both of them using
DimRepr, but with aShapetrait and aStridetrait we would retain more flexibility and it would probably allow us to implement more specific functionality for each one of them separately, without causing confusion to the end user.MRuecklCC commentedon Oct 13, 2022
Having worked with numpy and also some Eigen3 in C++ I was wondering if there are plans to support having the size (of some dimensions) as part of the type.
If I recall correctly it was/is possible in Eigen3 to have a type that has fixed (compile time) size in some dimensions and dynamic size in others.
Something along with
let a : Array<f32, Dim<[IxDyn, 5, IxDyn, 2]> = .... I think Eigen3 usedisizefor the shapes and the special value-1to indicate a dynamic dimension in the types.Edit: AFAI this is not (yet) possible with rust, as it would either require variadic const generics or const generics to support more than
usize,boolandcharvalues. -- But one can of course work around the missing variadics by handcoding up to a reasonal number:struct Dim2<const S1:usize, const S2:usize> ...