Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

NdArray trait #339

Open
bluss opened this issue Aug 13, 2017 · 5 comments
Open

NdArray trait #339

bluss opened this issue Aug 13, 2017 · 5 comments

Comments

@bluss
Copy link
Member

bluss commented Aug 13, 2017

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
@termoshtt
Copy link
Member

termoshtt commented Aug 14, 2017

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.

@bluss
Copy link
Member Author

bluss commented Aug 14, 2017

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

@bluss
Copy link
Member Author

bluss commented Aug 14, 2017

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
Copy link
Member Author

bluss commented Sep 24, 2019

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.

@akern40
Copy link
Collaborator

akern40 commented Dec 26, 2024

@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?

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Projects
None yet
Development

No branches or pull requests

3 participants