Compositing with Index and math traits


#1

As a way of learning about Rust’s traits and generics I’m implementing a basic vector math library. My current idea for the vectors themselves was to create a Vector trait that defined and implemented all operations on vectors (dot, cross, …) and then have specific vector types (Float2, Float3, Uint3, …) implement that trait. This would let me only implement Vector methods and operator overloading on the actual Vector trait and then have some tiny TypeN, e.g. Float3, structs that implement the Vector trait. This is working so far for a specific Float3, but not quite as intended. Ideally I would like to use the std::ops::Index trait for indexing into the Vectors elements,

The code so far

pub trait Vector : Index<usize> {
    fn element_count(&self) -> usize;

    fn dot(&self, rhs: &Vector) -> f64 {
        let mut res = 0.0;
        for i in 0..self.element_count() {
            res += self[i] * rhs[i];
        }
        res
    }
}

#[derive(Copy, Clone)]
pub struct Float3 {
    pub x: f64,
    pub y: f64,
    pub z: f64,
}

impl Index<usize> for Float3 {
    type Output = f64;

    #[inline]
    fn index<'a>(&'a self, i: &usize) -> &'a f64 {
        let slice: &[f64; 3] = unsafe { mem::transmute(self) };
        &slice[*i]
    }
}

impl Vector for Float3 {
    fn element_count(&self) -> usize {
        3
    }
}

however, this fails with the compiler errors

error: binary operation * cannot be applied to type <Self as core::ops::Index<usize>>::Output

error: the value of the associated type Output (from the trait core::ops::Index) must be specified

which makes perfect sense as output haven’t been specified, but how do I do that? Is is possible to specify Index::Output as part of the Vector trait?

And what happens when I want to add the add, sub, mul and other traits that have their own Output type to Vector? Is that even possible? Or can I implement the Index and math operator traits for Vector instead of ‘inherit’ from them?

Is what I’m trying to do possible in Rust or is there a better / more idiomatic implementation.


#2

You have to specify Output as something that can be converted to f64, for example using ToPrimitive:

pub trait Vector : Index<usize> where
    <Self as Index<usize>>::Output: ToPrimitive
{
    fn element_count(&self) -> usize;

    fn dot<O: ToPrimitive>(&self, rhs: &Vector<Output=O>) -> f64 {
        let mut res = 0.0;
        for i in 0..self.element_count() {
            if let (Some(lhs), Some(rhs)) = (self[i].to_f64(), rhs[i].to_f64()) {
                res += lhs * rhs;
            } else {
                //Oops!
            }
        }
        res
    }
}

This is far from the most solid solution, since it may fail. An alternative would be to use a custom trait with the restriction that it cannot fail. You will figure something out.


#3

I dug further into this and found out that I can implement the Index trait for Vector instead of inheriting from it.
If I really needed to inherit from Index I found out that it is possible to specify the output type when inheriting, so there is no issue when inheriting from multiple traits that define output.


#4

Nice! An alternative, that I came to think about, is to make the output of dot be the same as Index::Output and make it implement the Zero, Add and Mul traits (or just Add and Mul and initialize with the first element). Just a thought :smile: