ndarray icon indicating copy to clipboard operation
ndarray copied to clipboard

`NdArray` trait

Open bluss opened this issue 8 years ago • 5 comments

We need to traitify the array types, so that we shift from using ArrayBase for generic programming to something more flexible.

Imagine a sketch:

pub fn std_axis<T>(array: T) -> Array<T::Elem, T::Dim::Smaller>
    where T: NdArray,
          T::Elem: Num,
{
     // implementation here.
}

Foreseen problems: How do we conditionalize methods?

  • Can mutate the axes or view
  • Can mutate elements
  • etc

bluss avatar Aug 13 '17 16:08 bluss

I agree to introduce such trait. My current generic code typically has the following form:

pub fn func<A, S1, S2>(a: &mut ArrayBase<S1, Ix2>, b: &ArrayBase<S2, Ix2>)
where
  A: LinalgScalar,
  S1: DataMut<Elem=A>,
  S2: Data<Elem=A>,
{
   ...
}

In my understanding, NdArray trait would rewrite it into following:

pub fn func<K, A1, A2>(a: &mut A1, b: &A2)
where
  K: LinalgScalar,
  A1: NdArrayMut<K, Ix2>,
  A2: NdArray<K, Ix2>,
{
   ...
}

I think Data* traits should be hidden from end users.

termoshtt avatar Aug 14 '17 02:08 termoshtt

Good idea there too: maybe a type parameter for element and dimension is better than associated types. Best to try both..

bluss avatar Aug 14 '17 14:08 bluss

Logically they are associated types:

A specific type like Array<f32, Ix2> has an assoc. type for the element (f32) because it is given from the type uniquely.

bluss avatar Aug 14 '17 14:08 bluss

Here are some tantalizing examples of what impl NdArray can look like. This kind of interface looks very flexible!

Instead of dynamic typing, we get abstract types, and we can pick and choose which of the associated types of the array should be abstract for each time we use the trait. (It will make sense when you look at these examples.)

Written for playground which as of this writing uses ndarray 0.12. (playground link)

use ndarray::prelude::*;
use ndarray::Data;
use ndarray::Zip;
use num::{Zero, One};

// Usage examples

pub fn example_longest_axis(array: impl NdArray) -> Option<Axis> {
    array.as_array().axes().max_by_key(|ax| ax.len()).map(|ax| ax.axis())
}

pub fn example_sum(array: impl NdArray<Item=f64>) -> f64 {
    array.as_array().sum()
}

pub fn example_generic_sum<T>(array: impl NdArray<Item=T>) -> T
    where T: Clone + Zero + One
{
    array.as_array().sum()
}

pub fn example_index_2d(array: impl NdArray<Dim=Ix2, Item=f64>) {
    Zip::indexed(array.as_array()).apply(|(_i, _j), _elt| {
        
    });
}

// The NdArray trait has associated types for
// Dimension,
// Data storage,
// Item (Element type)

// But each usage can pick which of these should be abstracted or explicit

pub trait NdArray {
    type Dim: Dimension;
    type Data: Data<Elem=Self::Item>;
    type Item;
    
    // It is not at all certain, that these are the methods
    // that should be on such a trait. But it's representative
    // of what kind of access we have.
    fn as_array(&self) -> &ArrayBase<Self::Data, Self::Dim>;
    fn view(&self) -> ArrayView<Self::Item, Self::Dim>;
}

pub trait NdArrayMut : NdArray {
    fn as_array_mut(&mut self) -> &mut ArrayBase<Self::Data, Self::Dim>;
    fn view_mut(&mut self) -> ArrayViewMut<Self::Item, Self::Dim>;
}

pub trait NdArrayOwned : NdArrayMut {
    /* methods */
}

impl<S, D> NdArray for ArrayBase<S, D>
    where S: Data,
          D: Dimension,
{
    type Dim = D;
    type Data = S;
    type Item = S::Elem;

    fn as_array(&self) -> &Self { self }
    fn view(&self) -> ArrayView<Self::Item, Self::Dim> { self.view() }
}

impl<'a, T> NdArray for &'a T
where T: NdArray,
{
    type Dim = T::Dim;
    type Data = T::Data;
    type Item = T::Item;

    fn as_array(&self) -> &ArrayBase<Self::Data, Self::Dim> { (**self).as_array() }
    fn view(&self) -> ArrayView<Self::Item, Self::Dim> { (**self).view() }
}

fn main() {
    type A = Array2<f64>;
    let a = A::from_shape_fn((5, 10), |(i, j)| i as f64 / (j + 1) as f64);
    println!("longest axis={:?}", example_longest_axis(&a));
    println!("sum={:.3}", example_sum(&a));
    println!("sum={:.3}", example_generic_sum(&a));
    example_index_2d(&a);
}

If this is done, I'd think it should not just be a convenient trait - it should really be an overhaul of the whole crate, so that it is all usable, using the traits as starting point instead of the inherent methods. I don't have a plan or a PoC that can show that it works.

bluss avatar Sep 24 '19 20:09 bluss

@bluss do you have any thoughts about how this kind of trait might interact with the ArrayRef type created in #1440? The view method would be covered by #1469 (assuming NdArray: ArrayLike, which I think makes sense) but the as_array method can't return ArrayBase unless ArrayRef doesn't implement NdArray, which just seems... wrong.

My understanding is that the advantage of this trait over ArrayRef is abstract types, right?

akern40 avatar Dec 26 '24 04:12 akern40