Idea on overloading of value assignment


#1

Right now we have operators like += which can be overloaded using the AddAssign trait. Assignment cannot be overloaded, and I agree strongly with that as it would conflict with moving in statements like a = b.

However, sometimes it is useful to be able to overload assignment of values. As an example, if reusable_string is a mutable String, you can append with a simple operator using reusable_string += string_slice;, but you cannot similarly set the value with a simple operator. To do that you would need to write reusable_string.clear(); reusable_string += string_slice;, but that is not as ergonomic.

Would it make sense to introduce another operator, for example :=, for exactly this kind of thing? It would be overloadable with a trait such as ValAssign which means assign just the value, not the whole structure, and it kind of looks like the other x= operators with XAssign traits. Then reusable_string := string_slice; would leave reusable_string with the same value as reusable_string.clear(); reusable_string += string_slice; but it avoids unnecessary reallocation.

Apart from String (which I used as an example), this could be useful for mathematics constructs such as large matrices and bignums. For example in the bignum crate Rug, I have a trait Assign which serves exactly that purpose, but of course it does not have a nice operator to go with it.

(As an extension, the operator could then be used in let statements to create an object from another object. Keeping with String for examples, to me let a: String := string_slice; is nicer than let a: String = string_slice.into();. It makes sense to me because there a is taking just the value of string_slice, so it kind of aligns with “value assignment.”)


#2

What’s wrong with the existing reused_string = some_string?

If you don’t want to move the RHS of the assignment, you need to copy all of its bytes anyway.

If you also want to avoid the memory allocation, you can use Clone::clone_from(), e.g.: reusable_string.clone_from(&some_string);. This is available today, just as a method call and not as an operator. (And I reckon this shouldn’t need yet another operator. Rust is already heavy on sigils.)


#3

In my example some_string should have been string_slice. I’ll edit.


#4

On nightly you can do string_slice.clone_into(&mut string_slice);, though that has some opportunities for improvement. I’d love to see a better library design for it, but haven’t been able to think of one…


#5

Is that better than clone_from() in some way? Because clone_from() doesn’t even require nightly, and Vec::clone_from() seems to be implemented in terms of clone_into() right now.


#6

I find the name clone_into unfortunate, as to me cloning means making a T from another T, not making an Owned from a Borrow, if anything it would be named copy_into. Which still leaves the issue of the directionality mentioned in the github issue.

What I’m talking about is not exactly this, however. I used String as an example as it is the place in the standard library where AddAssign performs better than Add and an assignment. The use case I had in mind was more stuff like bignums and matrices.

For another example, currently the bignum rug::Integer implements AddAssign<u32> so that I can i += 3u32. It also implements rug::Assign<u32> so I can i.assign(3u32). Here 3u32 is not a borrowed version of an Integer being copied into an owned Integer.

I could also imagine it being useful in a matrix library for things like matrix.assign(Eye) where Eye can be a unit type used to set any matrix to the identity matrix.

Having a trait named exactly Assign in the standard library could be confusing as it might be mistaken for the assignment operator which has move semantics and which rightly cannot be overloaded.

My question here is whether it makes sense to include a similar ValAssign in the standard library, which is named with a Val prefix to avoid the move confusion. It could then not only be used for things like owned.val_assign(borrowed) (which has the correct directionality), but also in other use cases, basically anywhere where AddAssign makes sense, so does ValAssign. Having an operator would improve the ergonomics here; i := 3u32 and matrix := Eye look much nicer than i.val_assign(3u32) and matrix.val_assign(Eye).


#7

That is sad, but I don’t think it warrants the introduction of a completely new operator. (I.e. I could trivially counter this argument by saying that "I find the := operator notation unfortunate, and prefer clone_from".)

Sorry, I’m afraid I’m not following. If we already have AddAssign, we should just be using += in cases where it makes sense from a performance point of view (e.g. instead of … = … + … in most cases). Why do we need another trait and another operator for that?

Or are you suggesting that foo := foo + bar be rewritten as foo += bar magically? I doubt that is a good idea, it’s very counter-intuitive for those who know the difference between + and +=.

I don’t think that an “assignment-like” operator is well-suited for performing conversions between types. If what you want isn’t performing a simple copy, but a conversion followed by (or happening in parallel with) a copy, then that fact should be very clear, and it would probably warrant its type-specific (i.e. non-generic) method on its own, or at most a trait method. It should definitely not look like an assignment. I.e. I disagree that "i := 3u32 and matrix := Eye look much nicer". To me, they would only look more confusing.

Incidentally, the word “value” is usually associated with move semantics in Rust. Therefore, even if such an operator were added, the name shouldn’t include “val” but something that reflects the fact that it contains a conversion, i.e. a nontrivial change in type and a potential performance-intensive operation.


#8

No, I’m not suggesting that! I only brought up AddAssign because ValAssign is in some ways similar to it, and can be seen as (a) a clear or a setting to zero followed by (b) AddAssign.

This is more what I’m talking about; it can be described as a set or copy from another type. (If the rhs is a borrow of the same type, that is ValAssign<&T> for T, it would act just like clone_from, so it wouldn’t add anything useful.) For example to set i to 3u32, you could either (a) write i = 3u32.into() which drops the old contents of i with its allocation and allocates a new Integer, or (b) use i.val_assign(3u32), which does not drop any i or any allocation and simply copies the value into it much more efficiently. And this can be implemented for multiple source types, just like for example AddAssign<Rhs> can be implemented with multiple rhs types for a single destination type.


#9

Somewhat related:

There are definitely people that think +(String, &str) was a mistake (and along with it, +=(&mut String, &str), as it doesn’t fit the mathematical meaning of +. Historically, operator overloading, and especially when “nonstandard” operators are used outside the set of {+,-,/,*}, has led to some very hard-to-read code. Any time you bring up using operators, you’re going to face an uphill battle convincing people that the use of operators is valid for your proposed use case. This is even if they’re an addendum to the main point; the easier a point is to argue, the more it will be argued, whether it’s a main point or not.

On top of this, Rust has a focus on being explicit (or rather, locally apparent) about behavior, and towards that point doesn’t allow integer conversions silently (instead requiring as or .[try_]into(). As you’ve described it, your proposed operator does “conversion” of a sort behind the scenes and therefor obscures the behavior from the reader.

That being said, what you want has definite parallels with “placement in”, in that what it wants to do is not conversion (otherwise place = value.into() would be fine), but rather it wants to put a value into the old allocation.

let x: BigNum = default();
x <- 5;
let x: String = default();
x <- "wow";

(note: just applying the idea, these are not necessarily versions of placement in that existed before it was unaccepted.)

The key point to push here is that you are trying to reuse the allocation. Framing it as clear(); += may be correct for numerical types (and coincidentally for the “abuse” of summation for String), but isn’t the operation you’re going for, so only serves to muddy your point.

clone_from is what you want, but you want one that’s more generic such that it doesn’t just operate over a borrowed version of Self, but can accept the “stack” version as well. So String.clone_from(&str), which doesn’t work either.

I can see how a lack of some way to set the value of a bignum like resource other than it.set(5) would be useful; it could also be used for Cell and maybe many other container types. (But since I learned in Java, I come from the world where there was no operator overloading, so bignum was just always done with methods.)

TL;DR: presenting this as an operator and a muddiness around “convert then assign” are not helping your push.


#10

clone_from takes &Vec<T>; clone_into takes &[T]. (Same difference as .clone() vs .to_owned().)


#11

Yes, but that’s not “better” but “different”. I guess both can be useful in their respective use cases.


#12

I think “strictly more general, in a way necessary to work the types implied by the variable names in the post” is absolutely reasonable as “better”.