[Beginner] Adding Vectors

Vec<T> is more general here than a mathematics 1D vector.

But when T is a number type I would expect Add to be implemented in it.

Already fearing I would face the orphan rule since both items were non-local (I wasn't certain though), I tried to use something like this:

use core::ops::Add;

fn main() {
    impl Add<u32> for Vec<u32> {
      ......
    }
}

Which of course failed.

I double checked my remaining options by looking at the time crate implementing Add, which uses locally-defined type, and the a local trait in Unicode Segmentation.

So I guess my question would be, why was this operation left out of vectors? Was it just a hassle to implement?

And, in this case, is a best practice to define a VectorAdd trait, or a new MathVector type? (This is for learning purposes, but I am open to look at existing libraries for inspiration, if there is any you'd suggest.)

I suppose there may not be a simple answer but am curious how others think about it.

PS: I just realised that implementing Add to MathVector would conserve the use of +. So that's one difference.

A Vec is not at all like a vector in mathematics. "Vec" presumably following on from "vector" in C++ is simply a dynamically sizeable array where all the elements are stored contiguously in memory. Whereas vector in mathematics has a fixed number of elements (is that always true?). As such it makes no sense to be applying mathematical operations like "+" on a vec.

Fun fact: "It's called a vector because Alex Stepanov, the designer of the Standard Template Library (in C++), was looking for a name to distinguish it from built-in arrays. He admits now that he made a mistake, because mathematics already uses the term 'vector' for a fixed-length sequence of numbers."

std::Vec is not a type for the mathematical vector concept, it's really a dynamic array, it's a terminology clash, unfortunately, but it's not original for rust, the term is already well established in programming, long before rust came into existence, so it followed the convention.

if you want to write algorithms about mathematical vectors, better to use a linear algebra library, one of the most commonly used is nalgebra:

Currently, I'm trying this, but can't quite solve it yet. I should add, this part is new to me.

use core::ops::Add;

struct MathVector<T: Add> {
    vec: Vec<T>,
}

impl<T: Add> Add<T> for MathVector<T> {
    type Output = Self;
    fn add(self, other: Self) -> Self {
        // todo, just using to match the type
        other
    }
}
fn main() {}

Oh, I think I should've looked at the examples further down, with generics. My bad.

Should be

impl<T: Add<Output = T>> Add for MathVector<T> {
    type Output = Self;
    fn add(self, other: Self) -> Self {
        // todo, just using to match the type
        other
    }
}

Should vec![1, 2] + vec![3, 4] be vec![1, 2, 3, 4] or vec![4, 6]? Both have a context where they make more sense, thus std authors refuse to impose their choice.

nalgebra or ndarray.

You cannot implement Add for Vec.

Rust has the orphan rule: to implement a trait for a type, you must own either the trait or the type. Therefore, you can implement the standard Add trait for your own type, but not for Vec, because neither Add nor Vec belongs to your crate. Conversely, you can implement your own trait for Vec, because you own the trait.

This is my naive runnable code using the first strategy (implementing Add for a new type).

Now I can add vectors with +.

Here is version 2, in preparation to add other arithmetic operations (possibly).

I'd appreciate any ideas or improvements.

Playground

that's a good start but i'd suggest a few tweaks.

impl<T: Add<Output = T> + Copy> Add<&MathVector<T>> for MathVector<T> {
        type Output = Self;
        fn add(mut self, other: &Self) -> Self {
            let ll = self.len(); //left (self)
            let lr = self.len(); // right ()
            if ll != lr {
                let msg = format!("Length mismatch instance ({ll}), other ({lr})");
                panic!("{msg}")
            }
            iter::zip(self.vec.iter_mut(), other.vec.iter()).for_each(|(a, b)| *a = *a + *b);
            self
        }
    }

this allows to reuse the allocation of the original vector and to not consume the right adder so you could potentially reuse for multiple additions

fn main() {
    let v1 = MathVector::new(vec![1, 2, 3]);
    let v2 = MathVector::new(vec![3, 4, 5]);
    let v3 = MathVector::new(vec![6, 7, 8]);

    let bias = MathVector::new(vec![9, 10, 11]);

    let add1 = v1 + &bias;//reuses memory of v1
    let add2 = v2 + &bias;//reuses memory of v2
    let add3 = v3 + &bias;//reuses memory of v3

    println!("{add1:?}")
    println!("{add2:?}")
    println!("{add3:?}")
}

I was thinking of using AddAssign to handle that case (though it's not equivalent, it mutates the outside value).

However, if one is already transferring ownership, it makes sense the way you suggested. Thanks.

Subtraction gets harder, at least if I want to allow 1u32-2u32 and such operations.

PS: I know I can cast or use From. But the typing is harder.


#[derive(Debug)]
struct MathVector<T: Add + Copy> {
    vec: Vec<T>,
}

you should not put bounds on types unless you need said bounds to specify the type's layout.

there is no good reason why you should require Copy at all, after all MathVector isn't even Copy, despite the fact that MathVector<MathVector<u8>> is perfectly sensible.
but even for Add, other types may want to store MathVector<T>, and by putting Add on the struct you force them to add the bound everywhere which is not only very annoying but can actually break very reasonable code.
consider :

enum MathOrNormal<T> {
    Normal(Vec<T>),
    Math(MathVector<T>),
}

here a MathOrNormal<T> would need Add even if it is always normal, for no good reason.
so you simply need

#[derive(Debug, Clone)]
struct MathVector<T> {
    vec: Vec<T>,
}

your Add impl is also very much too restrictive.
you require all the values to be Ts, which closes you from many different valid inputs.

impl<T: Add<Output = T> + Copy> Add for MathVector<T> {
    type Output = Self;

instead do something like

impl<U, T: Add<U>> Add<MathVector<U>> for MathVector<T> {
    type Output = MathVector<<T as Add<U>>::Output>;
    fn add(self, other: MathVector<U>) -> Self::Output {
        self.assert_same_lenght(&other);
        let output: Vec<_> = iter::zip(self.vec, other.vec).map(|(a, b)| a + b).collect();
        MathVector::new(output)
    }
}

which will cover all the possible Adds.
but whenever possible you should also provide operations by reference, to avoid needless cloning . here are what they would look like :


impl<'a, U, T: Add<&'a U>> Add<&'a MathVector<U>> for MathVector<T> {
    type Output = MathVector<<T as Add<&'a U>>::Output>;
    fn add(self, other: &'a MathVector<U>) -> Self::Output {
        self.assert_same_lenght(&other);
        // the & should be second to try and reuse the owned memory
        let output = iter::zip(self.vec, &other.vec).map(|(a, b)| a + b).collect();
        MathVector::new(output)
    }
}


impl<'a, U, T> Add<MathVector<U>> for &'a MathVector<T> 
where 
    &'a T : Add<U>
{
    type Output = MathVector<<&'a T as Add<U>>::Output>;
    fn add(self, other: MathVector<U>) -> Self::Output {
        self.assert_same_lenght(&other);
        // the & should be second to try and reuse the owned memory
        let output = iter::zip(other.vec, &self.vec).map(|(b, a)| a + b).collect();
        MathVector::new(output)
    }
}


impl<'a, U, T> Add<&'a MathVector<U>> for &'a MathVector<T> 
where 
    &'a T : Add<&'a U>
{
    type Output = MathVector<<&'a T as Add<&'a U>>::Output>;
    fn add(self, other: &'a MathVector<U>) -> Self::Output {
        self.assert_same_lenght(&other);
        let output = iter::zip(&self.vec, &other.vec).map(|(a, b)| a + b).collect();
        MathVector::new(output)
    }
}

this allows adding two &MathVector<i32>s together for example

It depends on the use case, but I would be disinclined to use any (math) vector type that allows trying to add vectors of different lengths and panics when the lengths don’t match.

That's what most libraries in other languages do, though. If the vectors are of different dimensionality the addition can not be performed.

For example, 2D and 3D vectors can not be added in mathematics. The same is valid for tensors.

If the vectors in strongly-typed library are of different dimensionality, then it's a compile-time error to try and add them.

But vectors could change their length at runtime; can't they? I thought that would be a reason to keep it as a panic.

Also eventually a user may be getting numbers from a file. Again, that would occur at runtime.

note that the allocation is already reused because you are using a simple zip() + map() + collect() so the compiler can see that it can reuse the allocation

In a strongly-typed library, no, they don't. That's one of the differences between mathematical vectors and Vec<T>.

And then they will get an error (not a panic - a handleable error) while loading this file.

These vectors can change the length though, it will be allowed. Maybe your point stands, I don't see how would I do that though.

So by MathVector I mean that they have standard arithmetic properties (those for math-vectors), and can be extended, shrunk, concatenated etc. Otherwise, they are hard to use.

The reason I used it was (for Copy) because numbers are copy, and String is not, so at least it would flag that case as misuse. You'd still be able to use booleans, which I'd like to avoid.

(Without any trait bounds, anything is allowed as an instance. I don't want that)

I am unsure how to narrow down to properties of numbers, if that makes sense.

PS: Actually Add forbids bool as well.

the way to narrow down to properties of numbers is to not do it.
if you need Copy for something, require Copy where you need it.
if you don't need it, then don't require it. and if you need it in some places don't require it in the places where you don't (a small exception to that might be if that really helps you write much cleaner code you can have one trait amalgam, but i would recommend against it).

if a user wants to add a MathVector<String> to a MathVector<&str>`, just let them. you don't need to care.

notable BigInt, a famous non-std type used when you need very large and perfectly precise numbers, is not Copy