Why do I have to indicate pass by (simple) reference or by value in calling code?


#1

(Reposted from http://www.reddit.com/r/rust/comments/2oqtei)

Lets say I have some struct for 3D a geometric vector and some operations defined on this as follows:

struct Vec3 {
    pub x : f64,
    pub y : f64,
    pub z : f64,
}
impl Vec3 {
    fn dot(&self, rhs : &Vec3) -> f64 {
        self.x * rhs.x + self.y * rhs.y + self.z * rhs.z
    }
}
impl Sub<Vec3, Vec3> for Vec3 {
    fn sub(&self, rhs : &Vec3) -> Vec3 {
        Vec3 {x:self.x - rhs.x, y:self.y - rhs.y, z:self.z - rhs.z}
    }
}

I can do some calculations with this like so:

fn some_vector_calculation(v1: &Vec3, v2: &Vec3, v3: &Vec3) -> f64 {
    v1.dot(&(*v3 - *v2))
}
fn main() {
    let v1 = Vec3 {x:1., y:2., z:3.};
    let v2 = Vec3 {x:4., y:5., z:6.};
    let v3 = Vec3 {x:7., y:8., z:9.};
    println!("result={}", some_vector_calculation(&v1, &v2, &v3));

}

But what I’d like to be able to do is simplify some function calls and have this still work:

fn some_vector_calculation(v1: &Vec3, v2: &Vec3, v3: &Vec3) -> f64 {
    v1.dot(v3 - v2) // doesn't compile
}
fn main() {
    let v1 = Vec3 {x:1., y:2., z:3.};
    let v2 = Vec3 {x:4., y:5., z:6.};
    let v3 = Vec3 {x:7., y:8., z:9.};
    println!("result={}", some_vector_calculation(v1, v2, v3));  // doesn't compile
}

The point is that I don’t think that calling code should have to distinguish between function parameters passed by value and function parameters passed by simple immutable reference (with lifetime that doesn’t exit from the called functions scope) at the point of function call.

It’s not such a problem to write those stars and ampersands in the example code above, in order to get it to work, but I’m finding that when working with more ‘real’ code this can actually be quite a significant irritation and causes (I think) unnecessary development friction and code complexity.

I think that the distinction between pass by value and (simple) pass by reference is better considered as an implementation detail, which is essentially just defined within the scope of the called function.

Yes, this parameter passing behaviour can have performance implications, and so you could argue that you want to see this explicitly at the point of call for this reason, but I really think this kind of performance issue is better considered the responsibility of the called function (just as with other performance implications relating to the called function), and I don’t think that the ability to see whether or not a parameter is being passed by value or reference explicitly at call site is worth all the extra hassle that comes with this.

Importantly, if I want to change some function calling signature from pass by value to pass by reference (or vice versa) I don’t want to also have to go back to all the code that calls that function and update all this stuff for the change.

Note that this is like pass by value or pass by const reference in C++. In that case, of course, there is not the whole notion of borrowing and borrow checking, but in the specific (probably quite common) case where the borrow lifetime doesn’t exceed the called function it seems that this is essentially exactly the same thing, and for people used to programming in C++ I think that having to change the function call site according to which of these two parameter passing methods is chosen by the function will probably seem very clunky and annoying (as it does for me).

I don’t know anything about the implementation details of Rust, or what kind of actual concrete changes would need to be made to the language in order to change this function call behaviour, just speaking as a potential user of the language really at this point. (But I somehow really like what I have seen of Rust so far, and could be interested in getting more involved, if that could help to change stuff like this!)


#2

So you basically ask for automatic refencing and dereferencing. I have comment not directly related to that. You can implement Sub for &Vec3 and the * dereferences can go away. There is also overhaul of operator traits planned in the near future so this will look slightly different.

impl Sub<&Vec3, Vec3> for &Vec3 {

#3

Agree. Where you pass a mutable reference, explicit notation at the call site has its use, but where you pass by immutable reference (especially since Rust nicely guards against concurrent mutation of the passed parameter) having to explicitly reference at the call site — some_vector_calculation(&v1, &v2, &v3) — seems an unnecessary burden.


#4

Implicitly deciding between by-value and by-reference could make it harder to reason about the implications caused by Rust’s memory safety checking. Explicitly putting an & before the parameter lets you know that you can still use the value after passing it to the function (and that it’s not going to be moved):

let x = get_non_copyable_type();
foo(x); // Under this proposal, am I going to be able to
        // use `x` after this call? If it’s by reference, then
        // yes; otherwise, no.

A type like Vec3 can be used after being passed by value (because it implements Copy), but most of the time, that’s not the case. Also, if it is Copy, then you don’t normally need to pass it by reference anyway (and so in this case, probably wouldn’t).


#5

Rather than auto-referencing, I’d like to see &move references, which would have the exact same semantics as pass-by-value except that it just passes a reference to the existing storage location.

The only hitch to this is that Rust apparently already uses pass-by-reference behind the scenes for non-floating point, larger than pointer types… but I don’t believe that’s actually guaranteed anywhere.

I’m not sure how I feel about auto-dereferencing… &move might be a good idea here, since it shouldn’t have the same potential problems as auto-dereferencing other references.


#6

Note that there are already other situations where you get this kind of ‘automatic referencing’, and so this issue (not knowing if the object has been moved if you only look at the calling code) already applies, e.g.:

#[deriving(Show)]
struct BoxedInt {
    pub i : Box<int>
}

impl BoxedInt {
    fn show_yourself(&self) {
        println!("I am {}", self);
    }
    fn show_yourself2(self) {
        println!("I am {}", self);
    }
}

fn main() {
    let b = BoxedInt {i: box 6};
    b.show_yourself();
    b.show_yourself();
    b.show_yourself2();
// compile error, was moved in to previous call to show_yourself2
// but we can't see this in the show_yourself2() call syntax
    //b.show_yourself2();
}

The key point for me is just not to have to distinguish between (simple) pass by reference and (non-moving) pass by value at the call site, when these are the two parameter passing methods that would apply (as in the example code I posted initially). I guess it would be possible to limit ‘automatic referencing’ to only apply to copyable types, but don’t have enough experience with rust to have a good opinion about this detail…


#7

@P1start What is the fundamental difference in terms of explicitness between free functions and methods?

let x = get_non_copyable_type();
x.foo(); // Under the current rules, am I going to be able to
        // use `x` after this call? Does foo mutate x? Note, that methods are much more common than free functions in Rust.

The argument about preferable explicitness doesn’t sound very convincing after realizing the amount of referencing, slicing and similar operations hidden under the autoderef or things like BorrowFrom. Every time something explicit becomes too common, people find a way to sweep it under the carpet. So, maybe this particular case shouldn’t have been (I hope it’s the correct tense, I’ve selected it randomly) made explicit from the beginning? But anyway, solving this problem cleanly (especially in generics) would probably require some serious changes to the language like introducing non-first-class references, so we will likely have to live with the current rules.


#8

A couple of other thoughts with regards to this ‘hiding parameter move into function’ point.

  • The whole thing with parameter passing maybe passing by value (with a copy) and maybe moving (with invalidation of the source), depending on the type of the parameter, actually seems a bit odd to me.
  • (Seems like this has a lot of potential for confusion when writing generic stuff. I think I’d prefer to specify one of these parameter passing semantics and have this applied consistently to all types, with an error where it can’t be applied.)
  • But in terms of this specific discussion, I don’t think that the possibility for something to be moved into a function, without this being clearly visible in the calling code, actually bothers me too much in practice. Normally I would hope that there would be some kind of clue about this in the function name, but if not then the great thing about Rust is that it tells you about this kind of issue (attempt to use moved value) very clearly as soon as you try and compile your code, and then there is usually a pretty clear trail to follow chase down what is going on…