Overload `+` on `Vec` to support appending one element

Currently Add is implemented on Vec only to append slices:

impl<'a, T: Clone> Add<&'a [T]> for Vec<T> {
    type Result = Vec<T>;
    fn add(self, rhs: &'a [T]) -> Vec<T> { ... }
}

This is really convenient when Vec is used in folds as an accumulator. While sometimes map()+collect::<Vec<_>>() can be used instead of fold(), it is not always the case, for example, when there is something else in the accumulator. The same thing also holds for String.

However, this does require that the RHS must be a slice, which is not always possible. While you can write e.g.

let v = vec![1u, 2, 3];
v.iter().fold(Vec::new(), |a, &x| a + [x].as_slice())

it is really ugly. For strings, however, even this is not possible - you can’t turn a char into a slice in general without allocation. You have to drop to push() method which does not return the string back, so the closure must have two statements in it:

let s = "abcde".to_string();
s.chars().fold(String::new(), |mut a, c| { a.push(c); a });

If Vec<T> and String implemented Add<T>/Add<char>:

impl<T> Add<T> for Vec<T> {
    type Result = Vec<T>;
    fn add(self, rhs: T) -> Vec<T> {
        self.push(rhs);
        self
    }
}

(similarly for String), then the fold can be written just as

let s = "abcde".to_string();
s.chars().fold(String::new(), |a, c| a + c);

Other growable collections could probably be improved in a similar way.

What do you think?

1 Like

Using a mathematical operator for a mutating operation is a terribly bad idea.

7 Likes

While this technically mutates behind the scenes, it moves the vector by value so, from the user’s perspective, vec![1,2,3] + 4 creates an entirely new vector vec![1,2,3,4].

2 Likes

Python doesn’t do it, Haskell has a separate operator for it. Might be a bad idea for type inference reasons.

Good point. Personally I’d prefer to allow vec![1u,2,3] + [1u] (impl<T> Add<[T]> for Vec<T>); as far as I know, this shouldn’t require any allocation.

1 Like

FYI, you can use less horrible-looking a + &[x] instead (playpen)

This shouldn’t affect type inference at all - it’s just another trait implementation. The situation where the RHS can be either a container or an element of the same container is impossible - the former includes the latter: Vec<T> and T, String and char, etc.

Add<[T]> can’t be defined because add() method in Add<T> takes T by value, which means that it can’t have T: ?Sized in principle. Also currently vec![1, 2, 3] + [1u].as_slice() doesn’t require an allocation too.

Got it. I thought there was some way to write fn add<A: [uint]>(items: A) but I guess there isn’t.

Trivial example where this affects type inference: vec![] + vec![].

2 Likes

Please god no. Operator overloading is butts. Just call push and help us figure out a better general syntax for chaining method calls (with! { foo: pop(); push(1); push(2); } or some crap like that).

14 Likes

This also seems bad because it’d be overloading the addition operator for concatenation. I personally don’t like this for strings, either.

1 Like

I’m with Gankro. This kind of abuse of operator overloading is one of the things that makes C++ so horrible. Vec.push() has nothing to do with the mathematical concept of addition, and it has none of its properties. Operator overloading makes code harder to read / understand (for humans), and it makes compilers more complex and less efficient, since they need to figure out which op+ you mean.

So, I vote against op+ for Vec.

2 Likes

It’s not mutating, it consumes the original Vec.

There is an RFC somewhere for using the ++ operator for concatenation, following Haskell’s example. I prefer that approach because it doesn’t have any relation to the mathematical addition operator.

2 Likes

While appending to a list is different in some ways from addition, they are both operations on a monoid and so share e.g. the associative property. You can even define the natural numbers in terms of lists of 1s; then addition is concatenation.

That's not to say that + is necessarily a good list concatenation operator; aside from the mathematical similarities we do need to take into account what the operator actually makes humans think of.

vec![1, 2, 3] + 4 should produce vec![5, 6, 7]. :stuck_out_tongue_winking_eye:

P.S. The syntax I’d like to see is

vec![1, 2, 3].into_iter().fold(Vec::new(), |mut a, x| a `push` x)
2 Likes

This is what I proposed on a recent (pre-1.0) PR that was to remove the + op for Vecs and Strings. Having a non-commutative addition for vec types is a footgun.

:+1: for ++ or -- or ~~ or whatever operator we may come up with. Anything but +.

3 Likes

Somewhat OT but, is there any reason add wasn’t defined as follows where Other is defined to be either the LHS or the RHS (i.e. sidedness is undefined)?

trait Add<Other=Self> where Other: Add<Self, Output=Self::Output> {
    type Output;
    // ...
}

In math the operand order for addition is not important but the same goes for Mul.