Why do all three work in this example: take(), as_ref(), as_mut()

I'm trying to understand why the third option below works. Here is my thought process (rust newbie) and assumptions (which might be incorrect).

  • The first argument to writeln! must be a mutable reference.
  • With option 1, to_child is a mutable binding to an owned value, so Rust will take a mutable reference automatically because the binding was labeled as mut.
  • With option 2, to_child is an immutable binding (no mut keyword) to a mutable reference, so Rust doesn't do anything automatically as the variable is already a mutable reference.
  • With option 3, to_child is a mutable binding to an immutable reference, so Rust will change the immutable reference to a mutable reference because the binding has the mut keyword??? Is this why it works?

I think I'm overthinking this, but I've been reading answers about the differences between immutable/mutable bindings and immutable/mutable references, which clearly state they are all different from each other.

Any help would be appreciated!

use std::error::Error;
use std::io::prelude::*;
use std::process::{Command, Stdio};

fn main() -> Result<(), Box<dyn Error>> {
    let mut child = Command::new("grep")
        .arg("test")
        .stdin(Stdio::piped())
        .spawn()?;

    // Option 1: Take ownership of ChildStdin with mutable binding
    // let mut to_child = child.stdin.take().unwrap();

    // Option 2: Take mutable reference to ChildStdin with immutable binding
    // let to_child = child.stdin.as_mut().unwrap();

    // Option 3: Take immutable reference to ChildStdin with mutable binding
    let mut to_child = child.stdin.as_ref().unwrap();

    writeln!(to_child, "this is a test")?;

    drop(to_child);
    child.wait()?;
    Ok(())
}

Macros themselves aren't concerned with types. This call to writeln! expands to to_child.write_fmt(...), which resolves to Write::write_fmt, which takes a &mut self receiver. The receiver of a method call is one of the locations where Rust will automatically borrow (mutably if needed) an expression to make a reference if needed. (Note that this doesn't happen for the other arguments of a function call.) Of course, to mutably borrow a binding, the binding must be mutable. So your conclusion of how options 1 and 2 work is correct.

This works because the Write trait is implemented for &ChildStdin as well as ChildStdin, meaning the type &ChildStdin itself has a write_fmt(&mut self, ...) method. So this is actually equivalent to Option 1 with a different receiver type.

2 Likes

Thank you.

I think I understand your response, but to clarify my interpretation:

  1. Does option 2 work for the same reason as option 3? I.e., the Write trait is implemented for &ChildStdin? I'm assuming yes because in option 2, to_child is a mutable reference versus an owned value.

  2. If the Write trait was not implemented for &ChildStdin, but only for ChildStdin, would the same three options work? I.e., if the argument to Write::write_fmt takes a &mut self receiver, would rust deref the &ChildStdin to find the implementation for ChildStdin? And, if so, in option 2, since the receiver is already a mutable reference, it would meet the requirement? And, in option 3, it would just need to "upgrade" the immutable borrow to a mutable borrow?

Every time I think I finally understand how things work, I stumble across something that challenges that and makes me question everything all over again. I think Rust is particularly difficult for me because I have a hard time just accepting that something works as I want to know the details but that's a dark scary road sometimes in Rust!

No. When you call a method with a &self or &mut self receiver, Rust has the option to automatically insert a (mutable) borrow to produce the correct type, but it does not have to.

In option 1, the type of to_child is Child, so Rust must automatically insert a mutable borrow to get a &mut Child, the expected receiver type for impl Write for Child's write_fmt(&mut self, ...).

In option 2, the type of to_child is &mut Child, which is already the correct type so nothing needs to be done.

In option 3, the type of to_child is &Child, so Rust automatically mutably borrows to &mut &Child, which is the correct type when using the impl Write for &Child impl.

Now in option 2, Rust could do the same thing - mutably borrow to create a &mut &mut Child, and coerce that to &mut &Child. But it doesn't, because it finds the version using the plain &mut Child receiver type first.

(The exact order here, per the reference, is the compiler first tries to_child, then &to_child, then &mut to_child, then *to_child, then &*to_child, then &mut *to_child, then **to_child, then &**to_child... In option 2 the search terminates at the first step.)

Options 1 and 2 would still work because they don't interact with the &ChildStdin impl. Option 3 would not work, because you cannot convert a &T to &mut T. (In theory here, we could, because the binding is mutable and there would be no conflicting borrow, but in other cases this would be unsound as it could violate the rule that at any given time a value can only be mutably borrowed once).

2 Likes

No. You can't get a mutable reference through an immutable reference. That's the very point of the borrowing model.

1 Like

The above was the light bulb moment for me. I did not realize what was really happening in option 3 until I saw the &mut &Child. Thank you so much for taking the time to thoughtfully and thoroughly explain this. I'm sure I didn't make it easy for you!

1 Like

One last question. With the order above, in option 3 if the Write trait was not implemented for the &Child, why would it not work. In option 3, to_child is &Child and we need to get to a &mut Child in this case:

  • "compiler first tries to_child": Nope, &Child not a match for &mut Child
  • "then &to_child": Nope, &&Child not a match for &mut Child
  • "then &mut to_child": Nope, &mut &Child not a match for &mut Child
  • "then *to_child": Nope, Child not a match for &mut Child
  • "then &*to_child": Nope, &Child not a match for &mut Child
  • "then &mut *to_child": Match, right? &mut Child is a match for &mut Child

Thank you.

Well, we can simply check what happens if we try and do this conversion manually:

pub fn rematch<T>(child: &T) -> &mut T {
    &mut *child
}

Playground
As you can see, compiler is not happy:

error[E0596]: cannot borrow `*child` as mutable, as it is behind a `&` reference
 --> src/lib.rs:2:5
  |
1 | pub fn rematch<T>(child: &T) -> &mut T {
  |                          -- help: consider changing this to be a mutable reference: `&mut T`
2 |     &mut *child
  |     ^^^^^^^^^^^ `child` is a `&` reference, so the data it refers to cannot be borrowed as mutable

For more information about this error, try `rustc --explain E0596`.

And implicit conversion here is, correspondingly, forbidden too.

1 Like

I guess the part that I missed was this paragraph in the reference:

This process does not take into account the mutability or lifetime of the receiver, or whether a method is unsafe . Once a method is looked up, if it can't be called for one (or more) of those reasons, the result is a compiler error.

Thanks for everyone's help. Much appreciated!

This topic was automatically closed 90 days after the last reply. We invite you to open a new topic if you have further questions or comments.