3

So I've seen this question which explains how serde_json can both take Readers/Writers by reference or alternatively take ownership of them. Fair enough.

But I don't get how this works for Write - all of Write methods require a &mut self, so I'd think that if I pass a method that only knows its argument is a reference to something that is Write it can't do anything with it. But this example compiles and works just fine, even though I'm passing a non-mut ref to a method that, one way or another, ends up writing to the referenced file:

extern crate serde_json;

use std::fs::OpenOptions;
use std::error::Error;

fn main() -> Result<(), Box<dyn Error>> {
    let file = OpenOptions::new()
        .create(true)
        .write(true)
        .truncate(true)
        .open("/tmp/serde.json")?;
    // why does this work?
    serde_json::to_writer(&file, &10)?;
    Ok(())
}

I'm passing a &File - as expected, if I were to directly call any of Write's methods on a File it doesn't work:

use std::io::{self, Write};
use std::fs::OpenOptions;

fn main() -> io::Result<()> {
    let file = OpenOptions::new()
        .create(true)
        .write(true)
        .truncate(true)
        .open("/tmp/wtf")?;
    let file_ref = &file;
    // this complains about not having a mutable ref as expected
    file_ref.write(&[1,2,3])?;
    Ok(())
}
error[E0596]: cannot borrow `file_ref` as mutable, as it is not declared as mutable
  --> test.rs:12:5
   |
10 |     let file_ref = &file;
   |         -------- help: consider changing this to be mutable: `mut file_ref`
11 |     // this complains about not having a mutable ref as expected
12 |     file_ref.write(&[1,2,3])?;
   |     ^^^^^^^^ cannot borrow as mutable

So what gives? Is serde_json breaking the type system somehow, or is this an intentional feature of the type system? If it's the latter, how does it work and why does it work like that?

Cubic
  • 14,902
  • 5
  • 47
  • 92

1 Answers1

10

serde_json::to_writer's first parameter accepts any type that implements Write. One such value is &File.

This may be surprising, but the docs for File explicitly state that the mutability of the reference to a file has no bearing on whether the file will change or not:

Note that, although read and write methods require a &mut File, because of the interfaces for Read and Write, the holder of a &File can still modify the file, either through methods that take &File or by retrieving the underlying OS object and modifying the file that way. Additionally, many operating systems allow concurrent modification of files by different processes. Avoid assuming that holding a &File means that the file will not change.

This might have you asking - wait, I thought the methods on Write took &mut self? And they do! But in this case, Write is implemented for &File, so the type passed to Write is, somewhat confusingly, &mut &File (a mutable reference to an immutable reference).

This explains why your second example doesn't compile - you need to be able to take a mutable reference to &file, and you can't do that without the binding being mutable. This is an important thing to realize - the mutablilty of a binding and the mutability of the bound value are not necessarily the same.

This is hinted at in the error message when you run your code:

error[E0596]: cannot borrow `file_ref` as mutable, as it is not declared as mutable
  --> src/lib.rs:11:5
   |
10 |     let file_ref = &file;
   |         -------- help: consider changing this to be mutable: `mut file_ref`
11 |     file_ref.write(&[1,2,3])?;
   |     ^^^^^^^^ cannot borrow as mutable

error: aborting due to previous error

If you change let file_ref = &file; to let mut file_ref = &file;, your code compiles.

Joe Clay
  • 33,401
  • 4
  • 85
  • 85
  • 1
    Very nice explanation! It is worth noting that when writing a generic function that takes a `Read` or `Write` you should always take it by value, not by reference, just like `serde` does. – rodrigo Jul 27 '19 at 18:30
  • 1
    Yep - the [original question](https://stackoverflow.com/questions/51809603/why-does-serde-jsonfrom-reader-take-ownership-of-the-reader) that the asker linked has that as an answer as well :) – Joe Clay Jul 27 '19 at 18:33