How does application of *Assign operators work?

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.)

1 Like

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 :slightly_smiling_face:

3 Likes