Why are `&` and `mut` annotations required at the invocation (i.e. use site) of a function instead of implicit conversion?

Did you have a chance to play with the built-in Drop trait which is Rust's RAII mechanism?

If f(&x) could be written as f(x), I'd need to check the signature of fn f() to see where my x is dropped (whether within f(x) or not.)

Moderator note: Everyone: please stop the unstructured commentary. I'd like to remind everyone to remain kind and courteous to each other.

4 Likes

Agreed. And that is what I am trying to understand in more detail. We only see the end result and not all the reasoning that went into it.

When I am contemplating choosing the (recommended, if not required) language for let's say perhaps 100s of other developers, then I have a responsibillity to know the history so I can gauge the future.

Hopefully someday someone can write a book about Rust that covers many more such details. Threads like this, can help them write that book. And in the meantime, this discussion helps me and perhaps other readers.

If I am not mistaken based on what arielb1 wrote, f(&x) does not guarantee it is borrowed, thus you still need to look at the signature of fn f(). That is the point I made.

You are mistaken. When you write &x as an argument, x is not moved, period. What @arielb1 probably meant is that if x is already a reference, it doesn't matter if you write f(x) or f(&x) because Rust will remove layers of references as necessary, and also convert references of one type to references of another type as directed by Deref implementations.

Yep that is what I understood @arielb1 meant, thus I am not mistaken because afaik a reference is not dropped. So you still need to determine if x is a reference or look at the function declaration site. The locality of the f(x) is ambiguous and doesn't tell you that x may be dropped.

Edit: okay I see your point which is that f(&x) will insure there is no scenario where it can be dropped. But again, you will know if it is moved and thus potentially dropped if you try to access it and will get a compiler error. So what does this gain in terms of information? I had already made that point upthread as follows:

Sorry I haven't yet seen a counter-argument to it being pointless noise.

<subjective>I love annotating types. But I learned from past experience that my love for that was actually counter-productive when it became so noisy that all I see are annotations and my salient program semantics are buried in noise.

It is sort of like a control freak or manly thing where we want to feel we are putting up extra fortresses, but when they are ambiguous or redundant noise, we actually are harming the readability:</subjective>

Because it is generating more heat than light, I am going to leave the discussion of tone alone.

If you see an & in the code, you know that the object in question is being borrowed. If you do not see an &, then you know that it is being moved or copied, depending on whether the type implements Copy. Whether a type implements Copy is a fairly fundamental aspect of a type, so it's generally something you know about the type if you are at all familiar with working with it, while there can be many different functions that work with the type that you don't know about.

let mut a = vec![1, 2, 3];
let b = foo(a);

When reading this in isolation, in Rust as it currently stands, I know that foo consumes a, and thus I won't be able to use it later on. In Rust as you propose, if & and &mut were elided (or even if just & were elided, as you conceded that &mut might convey useful information), then reading this code I would not know if a were consumed or merely borrowed.

Which afair is not true for trait object arguments, thus invalidating
your point unless you are just referring to the borrowing semantic and
not the duplicitous meaning of & (tangentially which in my opinion may be confusing to new people learning the language).

I don't know what you're referring to about trait object arguments or "duplicious meaning of &" here. Can you elaborate? & always indicates a borrow. Because of deref coercions, it can also indicate a reference-type-to-reference-type coercion, but no matter what it indicates that what you're passing in is a borrowed reference and not moving an object.

There are three different kinds of entities involved in the development process. There's the person writing the code; their interests can sometimes be met by making it as easy as possible to write code. Having the compiler do more inference can sometimes make this job easier. There's the compiler. The compiler must be able to unambiguously translate any correct program into an executable that correctly follows the semantics of the language, and in Rust's case, also reject any code that it cannot guarantee will be translated into an executable according to the semantics of the language (at least, without the use of unsafe).

However, there's a third entity that you haven't considered, which is anyone else reading the code afterwards. This can be someone reviewing the code. This can be someone hunting for a bug. This can be you reading code you wrote a couple of years ago when planning to do a refactor.

To someone in that is just reading the code, foo(a) vs. foo(&a) says something very different. In one, I'm giving away ownership; in the other, I am giving an immutable borrowed reference. The & gives me, the reader, more information, without having to follow through to the declaration of foo.

For instance, let's say I have the following function

fn baz() {
    let mut a = vec![1, 2, 3];
    let b = foo(&a);
}

Then while reviewing code, I see that someone changed it to:

fn baz() {
    let mut a = vec![1, 2, 3];
    let b = bar(a);
}

When looking at that change, it makes it very clear that I am now passing ownership to bar, and thus consuming a. I might object in code review "no, we shouldn't be calling bar since we are about to do another change that will require more functions that process a, we should call bar_ref instead." Without that extra annotation, there wouldn't be a way to tell that foo and bar were taking the value in a different way, which would make this change a lot easier to miss.

1 Like

This is all quite dramatic - and still subjective - so I'm just going to recommend you follow the second part of @llogiq's advice and write a little more code in Rust. Then it could be easier for you to understand why we value these annotations, and why they are definitely not "redundant noise" to us.

1 Like

Are trait objects always borrowed?

Also it initially struck me as confusing the multifarious usage of & (and also the confusion with the meaning of & in C/C++).

But why do we need to know? If we access it later in the code, then we know it was borrowed or copied. If we don't, then why do we need to know? Due to deref coercion, I presume a &x will be coerced to a copy if the function doesn't require a reference and the type is a Copy type?

Apparently that is not true if the x is already a borrowed reference.

<subjective>You see it is already a confusing mess of noise when it is so complex that even you can't distill it to simple to remember rules. That is what initially stuck out for me like a wart on Rust. Far too much complexity of intersecting cases for what should be a simple function call semantic.</subjective>

Did you not see I put my comment inside of <subjective></subjective>? Why are you continuing to try to chase me away? Please respond with your subjective opinion if you would like to and keep my personal life out of your comments. Please.

I don't need your advice about when I should code nor about your opinion of my me as a justification for why Rust's feature is desireable. Your opinion about why it is desirable or why my opinion is invalid is welcome. But my personal priorities are not a valid justification for anything about Rust.

The & in f(&x) is undeniably redundant. This can be seen by the fact that it's not required for &self or &mut self arguments. For example, explicit borrows are required if I write Vec::len(&v) or Vec::sort(&mut v), but are inserted by the compiler if I write v.len() or v.sort() (which are simply sugar for the same things).

Is it noise? No, it provides information to the reader. It says "x is not moved into f." Yes, this information is also available elsewhere (as I said, it is redundant), but readability and error prevention often benefit from locality.

Does the redundancy have downsides? Yes, and Rust explicitly concedes that by not requiring it for method calls. Auto-borrow of self exists because v.len() is more concise and "prettier" than (&v).len().

Would auto-borrow also have downsides? Yes. I've spent a lot of time helping and teaching new Rust programmers. Many difficulties reading Rust code come from the ambiguity in method call syntax, for example the fact that it's not clear without consulting the method signature whether iter.take(5) mutates or consumes iter. Rust made a conscious choice to limit the places where this particular inference (with both its benefits and its drawbacks) can occur.

No, you can have owned pointers to trait objects, like Box<Trait> or Rc<Trait>.

It is true. If x is a borrowed reference then it is copied. (&T implements Copy.) Reference types obey the same rules as all other types.

If the type has destructors, then it can matter greatly. For example if I create a CString and then pass cstring.as_ptr() to some C code, then I need to know that the destructor doesn't run while the C code can still access that raw pointer.

Deref coercion will only coerce from one reference type to another. It won't convert a reference to a non-reference, or vice-versa. See RFC 241 for details. (Again, the one exception is for method call syntax, where both auto-deref and auto-borrow apply to the self argument.)

2 Likes

Moderator note: Please do not leave any personal barbs in your replies, and also please try not to react to any barbs you perceive in others' comments. Flag comments that are inappropriate, then move on. If this continues to turn into an argument about tone, then we will ask that all participants take it elsewhere.

3 Likes

Actually that is the case that I am most concerned about and I think you've conceived the problem opposite of what it is in reality. In reality, the creator of the code knows what those annotations signify, but the 3rd party reader will see ambiguity that the creator had filtered out due to their familiarity with the code.

I am actually thinking much further down the line when you get more experience with n00bs reading Rust code and no one can make a simple set of rules for what those & all over the call sites actually signify unambiguously.

I keep asking what reason would anyone need to know? If it is moved and you try to access it, the compiler will balk.

Please I never argued to remove the mut annotation from the call site. I am talking about &, not &mut.

Are you telling me that if I have borrowed reference then I have to remember to litter every call site with & to tell it to not copy. That is the opposite of the default action one would intuitively expect. If you are passing around references, then you typically don't want to make copies, otherwise you'd be passing around the Copy type that is referenced to begin with. Oh the performance hit I see coming with this once programmers forget to annotate with & and get silent copies.

<subjective>It is corner case stuff like this that just boggles my mind when I try to wrap my mind around how Rust would be intuitive. Losing the forest in the trees. The salient understanding that should be a priority becomes a lower priority due to needing to justify redundant annotation ... or something like that....</subjective>

No. You can have owned trait objects, like Box<Foo>.

trait Foo {}
struct Bar;

let f: Box<Foo> = Box::new(Bar);

These can be passed into a function as an owned object:

fn blah(_x: Box<Foo>) {}
blah(f);

These can also be passed in by reference. Generally, when a function takes a trait object by reference, you want to take the most general type of reference possible:

fn zot(_x: &Foo) {}

If we didn't have deref coercions, this would be an error:

zot(&f);

You would have to write this instead:

zot(&*f);

Since the * is pretty much "useless noise", the concept of deref coercions were added to make it possible to eliminate everything but the &; but because the & communicates useful information about whether you are passing ownership or borrowing, it was decided not to elide that as well.

Right. However, if x is already a borrowed reference, that information is declared within the lexical scope in which x is defined. That is a local piece of information. So by just looking at the one function in which x is defined, you have all of the information you need to know.

Yes, in Rust there is more to think about when passing an argument to a function, or defining the interface to a function, than there is in many other languages. In any language which uses GC, there is no such thing as a reference; there are only pointers with shared ownership, and the GC is responsible for cleaning everything up once none of them are accessible. So there's pretty much just one way to pass in an argument to a function. In GC'd languages, whether the function you are passing the argument to is expected to "own" it, whether it's expected to mutate it, or whether the function is expected to only manipulate it immutably is merely a matter of API contract (or in purely functional languages, since you can only access it immutably, there's no real difference between owning it and a shared immutable reference).

However, all of these same considerations you have in Rust are things that you do have to think about in a language like C or C++; you can pass borrowed pointers in to functions, and part of the API contract involves what those functions can do with them; whether the object is only expected to be valid for the duration of the function call, or whether its lifetime is tied to that of some other object, or whether it is being passed in to be owned and cleaned up by the called function. However, in C or C++, that API contract is not written down anywhere that is checkable by the compiler; it has to just be written down in in prose in documentation, and expect programmers to get it right without much help (C++ does now provide the help of unique_ptr vs. references to indicate an ownership vs. borrow relationship, but many other aspects are not checked).

So, what Rust is doing is making these considerations you need to make in C or C++ explicit and checked by the compiler. It is somewhat more complicated than just having one way to pass an argument to a function, but a modest amount of inference and coercion has been added to make it a bit easier to work with. The amount that has been added helps to balance between the extra verbosity or "noise" of having annotations which could be elided, with the explicitness that makes it more clear exactly what's going on.

No. If x is a reference, say of type &T, then calling f(x) will copy - the reference, i.e. the pointer. Nothing special happening here, no big structs being copied silently.

So the issue is unsafe code.

<subjective>So we litter all our safe code for the unsafe case.</subjective>

I think I will bow out of the discussion, because I think I would first need to be convinced of the value of this very complex low-level structuring of resource lifetimes, before I would invest the effort to think about how I might design it to have fewer of what I perceive IN MY OPINION to be warts and corner cases. At this time, I am thinking GC is what I want for 80-95% of my code and the other 5-20% is going to need to be very carefully coded in any case.

Actually I had started to think about how I would have designed it differently, because for example & at the call site to me means "take the address of" so it appears to mean pass a pointer to the function, thus it appears to mean "move" and not "borrow". The & the function declaration site should be an * to be more consistent with C/C++ lineage so as to avoid confusion due to ingrained habit.

My most significant thought was that perhaps move should be the annotated case! Because moving should be rarer. I suspect that would clean up most of the noise, but I didn't think through yet all the issues with the change.


I didn't intend to offer my opinion on this. That is why I had hinted to @arielb1 that I felt the decision was already made and I wasn't interested in pushing it.

Given @arielb1 wanted to discuss it further, I have since explained some of my thoughts and strong doubts. That is sufficient I think so the viewpoints are out there. This low-level resource lifetime feature really isn't a priority of mine at this time. So I am wasting time here if I discuss further. Hopefully you can take my viewpoints into account FWIW. Thanks.

No harm is intended to Rust nor anyone here. I sincerely just want to work out what are the best tools or how tools fit various use cases. No ill will intended. I knew I didn't want to discuss this because I knew it was a topic that would become inflamed very quickly.

No, what he's saying is that the type &T is copied; the reference itself is copied.

Remember, a reference is just a pointer. An &T reference is an immutable, and thus shared reference. It is valid to have multiple &T references to the same object live at the same time, because none of them can mutate T (unless T implements some kind of interior mutability that adds dynamic checking or protection against multiple concurrent mutable accesses, in which case it is valid to mutate T through shared references).

So, the type &T itself implements Copy; you can copy the pointer around as much as you want, as long as those copies obey the restrictions that the borrow checker places on them, and all you are doing is copying a pointer.

Don't worry; there is no major expense when passing a value of a reference type to a function. It works like you would expect.

Rust's semantics are fairly intuitive if you've done some programming in C or C++, especially modern C++ with smart pointer types like shared_ptr and unique_ptr. If you haven't done that, and in particular if you've only ever written code in garbage collected languages, I agree, they will take a little more time to wrap your head around.

1 Like

I wrote a million user application in C++ in 1999.

But didn't use those. I had to roll my own reference counting pointer.