Use the same variable in multiple function parameters

Code that doesn't work:

trait Foo {}
trait Bar {}
struct MyStruct {}
impl Foo for MyStruct {}
impl Bar for MyStruct {}

fn f(_: &mut dyn Foo, _: &mut dyn Bar) {}

fn main() {
    let mut my_struct = MyStruct {};
    f(&mut my_struct, &mut my_struct); // error: cannot borrow `my_struct` ...
}

What is idiomatic Rust for this use case? Can I do better than adding another function with Foo + Bar trait bound on its parameter? What if the function takes even more parameters, the number of additional functions needed doesn't look like linear (bounded by 2^n-n, but don't trust this number as I could be wrong): In case of having 3 parameters, the first and the second could be the same, or the first and the third, or the second and the third, or all three are the same, etc.

fn f<T: Foo + Bar>(_: &mut T) {}

fn main() {
    let mut my_struct = MyStruct {};
    f(&mut my_struct);
}

Rust Playground

In general, mutating the same thing has to be handled differently than mutating different things, so you would need different functions anyway.

If that is not the case then possibly you shouldn't be passing &mut at all.

4 Likes

Yeah this is the exact thing I want to do better than. You still need the 2-parameter function because the two arguments may or may not be the same. And the number of extra functions required grows rapidly with more parameters.

std::io::copy needs &mut on both parameters. But std::io::copy does not have 1-parameter version, so it's not possible to echo the same stream. This use is perfectly valid but at odds with Rust ownership model. The function signature excludes this valid use.

If the stream supports this use-case, it will most likely implement Read and Write on shared references to itself, therefore you'll be able to call std::io::copy(&mut &stream, &mut &stream), which works. What kind of stream are you talking about, exactly?

3 Likes

It may also be clonable, or have a method to split it.

P.S. your link has an extra ` at the end of it.

That's even more likely, yes - Tokio's TcpStream is one such example.

Thanks, fixed.

TcpStream for example. But how a specific stream splits itself is circumventing not solving the issue. The issue is the function signature implies exclusivity among its parameters which is not intended.

If you wanna go the "split" way, the general solution requires a common approach to split an object into each trait it implements. I don't know such an implementation exists.

It general case, it is intended. You don't want to pass the same Vec<u8> as both reader and writer at the same time, since writer might reallocate, and reader would dangle.

4 Likes

OK phrase it the other way:

The issue is the function signature cannot imply non-exclusivity among its parameters when this is intended.

Of course, since it's not the function job. It can't distinguish whether the passed-in type can be read from and written to simultaneously, so it have to assume that it can't be, and if it can - the type itself must provide some way.

2 Likes

That notion give me headache.

The meaning of "copy" is that you start with one thing and end up with two things that look the same. Think copying a painting or a book. Or of course bit patterns inside computers. As the dictionary says "make a similar or identical version of". That of course requires two things, the thing you are copying and the thing that will become the copy. That is two parameters.

If there were only one parameter that would imply making a copy over the the original. Which is pointless of course.

The fact that it's allowed to read and write to the same file or socket handle in Unix or whatever and that other languages allow that is beside the point. And for sure under the hood the hardware that does the reading is different from the hardware that does the writing. The read and write operations in your code are not talking to the same thing. Rust is right here, other languages are wrong :slight_smile:

No, it's not – it's exactly the point that OP wants to write to the same stream that is being read from. It is hard to express with the shared-xor-mutable rule of the language, but it's a valid piece of functionality to want.

Of course, I'm not saying that sharing-xor-mutability should be compromised for such a special case. But I don't think it should be impossible to read from and write to the same stream.

A better API design would be for a readable-and-writable stream is to split it into two objects at the type level: a reader and a writer half. In that case, everyone would be happy – there would be no need for shared mutability, and the desired functionality could be achieved as well.

3 Likes

I understand that is the OP's point. What I was saying is that being able to read and write a file handle in Unix, or whatever, is beside the point of anything called "copy".

Indeed. Tokio provides for that. split in tokio::io - Rust

I don't think a change to copy is required or makes sense.

I was not arguing that.

Apparently, it is not, because that's exactly what std::io::copy() does. Writing the contents of one thing to another is copying. There's no problem with the nomenclature here.

Yes writing the contents of one thing to another is copying. But our OP wants to write the content of one thing to the same thing. That is not copying. That is a no op.

Perhaps std::io::echo

I guess the confusion stems from the "another" bit. Does it make sense to copy the file into itself (and not into another file), for example?

Exactly.

If you read my original post, it's not about any specific stream, but a general prototype where a single object implements multiple traits. Also the problem is not specific to any copy function.

In the specific case of TcpStream and std::io::copy(), the full duplex TcpStream can be viewed as one downstream and one upstream wrapped in a single struct. For copy, it makes sense if we think these two streams are not quite the same thing.

But splitting is not always easy or even possible. In general a struct can implement multiple traits, and the problematic function only pays attention to each trait. The function may not even know the argument types (I used trait objects to illustrate this). The function simply does what the traits say the trait object can do.