A friend of mine is learning Rust and wrote this code Rust Playground (NOTE: this will error due to needing to make it *self = in normalize()
#[derive(Copy, Clone, Debug)]
pub struct Vec3(pub f64, pub f64, pub f64);
impl std::ops::DivAssign<f64> for Vec3 {
fn div_assign(&mut self, rhs: f64) {
self.0 /= rhs;
self.1 /= rhs;
self.2 /= rhs;
}
}
impl Vec3 {
pub fn length(&self) -> f64 {
(self.0*self.0 + self.1*self.1 + self.2*self.2).sqrt()
}
pub fn normalize(&mut self)
{
self /= self.length();
}
}
He asked me why, in the normalize() method, does he have to dereference self when div_assign() is expecting a &mut self?
I don't really have a good answer for him. Intuitively to me it's because DivAssign is implemented for Vec3 and the compiler just knows that when it sees /= it calls div_assign() with a mutable reference to the left operand... but this seems a little unsatisfactory as an explanation. Is it just that assignment operators are handled specially for the sake of ergonomics? Or is there some other mechanism at work here that I don't understand?
Places are expressed using references in the APIs, so indeed it makes sense for …_assign to take a &mut self receiver.
That is, the rust unsugaring of +=-like methods is:
place += value;
// unsugars to:
AddAssign::add_assign(&mut place, value)
- Except for built-in types such as integers, which are special / magical and their sigil-based semantics are hard-coded in the compiler…
Rationale
The reason for that, is to mimic the non-generic code out there, especially in other languages, where one writes:
<place> += <value>;
and not:
&mut <place> += <value>;
// or, for languages with no `&mut`,
&<place> += <value>;
The most widespread example being:
i += 1;
and not:
&i += 1
I personally think that part of the confusion stems from the &mut self shorthand receiver syntax on methods, which hides that self has the type &mut Self and not the type Self.
That is, ask your friend if:
pub fn normalized (mut self) -> Self
{
&mut self /= self.length();
self
}
would have looked better to them?
Thanks for the detailed reply! I’m not quite following you on the last bit though - are you saying that &mut self in normalize is not actually equivalent to self: &mut Self?
I believe they're just pointing out that if things worked like this (didn't layer on the extra &mut):
fn normalize_self_in_some_other_world(&mut self) {
self /= self.length();
}
Then, if consistent, things would also work like this
fn normalized_copy_in_some_other_world(mut self) -> Self {
&mut self /= self.length();
self
}
And that's weird, as per the rationale.
(And as for the receiver, num.normalize_self() and num.normalized_copy() look similar, even though one takes &mut self and the other takes self by value. Which is which is "hidden" (not apparent) at the call site.)
Gotcha. Yes I agree the rationale is worth it.
Unfortunately it’s a bit tricky to explain to newbies to Rust who are already experienced C++ programmers how this works as they see “function takes a &mut but I have to deref and pass it a value?”
I’ve been writing Rust for long enough that I just don’t think about it until someone asks me why it works this way.
It's mainly the sugar of += which is the one to "blame", here:
pub fn normalize(&mut self)
{
DivAssign::div_assign(self, self.length());
}
works in the intuitive fashion.
It's just that /= is sugar which expects the lhs to be dereferenced, since it implicitly applies a &mut to it:
lhs /= rhs ≝ DivAssign::div_assign(&mut lhs, rhs)
so, with lhs = *self we get:
DivAssign::div_assign( self , rhs)
⇔ DivAssign::div_assign(&mut *self , rhs)
⇔ *self /= rhs
It's kind of similar to the ubiquitous parameter-by-reference sugar in C++ which does not need the caller to be adding & on its parameters. Rust almost never has that, except for some of the sigil-based operators, such as +=, or the comparison operators which implicitly apply & on both operands 