Use generic super traits in custom traits

Hello everyone
I have a question I can't find an aswer searching this forum

I want to create a Matrix trait that allows me to use generic types behaving like matrices. So I can use it like this:

fn do_some<T: Float, M: Matrix<T,R,C>>()  -> M {
    M::identity() * M::one()  // whatever
}

generic trait method

When the matrices operators are trait methods, everything works well. But the result is not convenient to use: no syntaxic operators.
The add method below defines that "types implenting Matrix<T,R,C> can sum to every other implementor of Matrix<T,R,C>"

pub trait Matrix<T, R, C>: 
	Clone
	+ Index<[usize; 2], Output=T>
{
	type Owned<R2, C2>: Matrix<T,R2,C2> + Default;
	fn matadd<M>(self, other: M) -> Self::Owned<R,C>
		where M: Matrix<T,R,C>;
}

generic super-trait bound

I want to move the add operator requirement to the super trait, so it can be the syntaxic Add operator
What I want is the following, however the compiler complains Add bound must be a concrete type

pub trait Matrix<T, R, C>: 
	Clone
	+ Index<[usize; 2], Output=T>
 	+ Add<impl Matrix<T,R,C>, Output=Self::Owned<R,C>> // this
// 	+ Add<Matrix<T,R,C>, Output=Self::Owned<R,C>>   // or this
{
	type Owned<R2, C2>: Matrix<T,R2,C2> + Default;
}

super-trait bound using trait arguments

I could give it a concrete type with generics, but this wouldn't give the same:
With the code below:

  • it means "types implementing Matrix<T,R,C> can sum to an other specific implementation of Matrix<T,R,C>"
  • rather than
    "types implenting Matrix<T,R,C> can sum to every other implementor of Matrix<T,R,C>"
pub trait Matrix<T, R, C, M>:  // adding a trait argument
	Clone
	+ Index<[usize; 2], Output=T>
 	+ Add<M, Output=Self::Owned<R,C>> // this is accepted but doesn't mean M is generic
 	where M: Matrix<T,R,C>
{
	type Owned<R2, C2>: Matrix<T,R2,C2> + Default;
}

So what I want is the same as a generic method in a trait, but for a super trait bound. Is there a way to acheive this ?

edit: I do not want to use a Add<dyn Trait> bound instead, because dynamic dispatch of methods leads to poor performances when called as often as a matrix or vector product. It prevents the compiler from inlining and optimizing the operation

Have you checked this? I've found that the compiler is actually pretty good at devirtualizing method calls when it's possible to statically determine the actual type present (i.e. you're not pulling it out of a heterogeneous container or similar).


A middle ground would be to use both your original method-only trait Matrix and a newtype wrapper that implements the arithmetic operator traits:

#[repr(transparent)] pub struct M<Mat>(pub Mat);

impl<T,R,C,Mat:Matrix<T,R,C>> Matrix<T,R,C> for M<Mat> {
    // ...
}

impl<T,R,C,M1,M2> Add<M2> for M<M1> where
    M1: Matrix<T,R,C>,
    M2: Matrix<T,R,C>
{
    type Output = Self::Owned<R,C>;
    fn add(self, other: M2)->Self::Owned<R,C> { self.matadd(other) }
}

// etc...

Have you checked this? I've found that the compiler is actually pretty good at devirtualizing method calls when it's possible

I did not tried actually. But such Matrix trait would be meant to write computation code in functions using generics and not in functions that immediately use one of its specializations, so is it a safe bet to assume the compiler will unroll the stack of generic functions to finally find which actual type is used behind the trait Matrix and then replace all pointer calls by direct calls ?

a newtype wrapper that implements the arithmetic operator traits

Well that's pretty much what nalgebra (among others) is doing for their storage backends. But it turns out that it is very boring to use for the user in generic code:

  • if M has methods wrapping actual methods of its underlying Matrix, then M must have the same generic parameters as the underlying object, so it means very long type names (and not intuitive) for the user.
    Like M<T, R, C, SomeMatrix<T, R, C>>
  • M shadows any non-wrapped methods of its underlying objects, so a user-made implementation of a Matrix can have no specific methods (since the user is in another crate it cannot add methods to M)
  • since storage-specific and conversion methods are only implemented on the underlying object, using a wrapper leads to a lot of wrapping-unwrapping-rewrapping user codelines, which makes code unclear when mixed with math operations.

So I would prefer avoid that solution :frowning_face:

This topic was automatically closed 90 days after the last reply. We invite you to open a new topic if you have further questions or comments.