Std question: File and fmt::Write

I've brushed up against this a couple of times now and while it doesn't really stop me from doing what I need to with the language I'm kind of just curious. Why does std::fs::File implement io::Write but not fmt::Write? Surely there's a technical reason that I'm missing and it isn't just an oversight.

Things tend just implement std::fmt::Write or std::io::Write since the write!() macro just calls write_fmt(), which both provide, and having both creates more potential for ambiguity. To quote from the documentation for write!():

A module can import both std::fmt::Write and std::io::Write and call write! on objects implementing either, as objects do not typically implement both.

The documentation for std::fmt::Write also provides guidance on which is chosen to be implemented:

If you only want to accept Unicode and you don’t need flushing, you should implement this trait; otherwise you should implement std::io::Write.

Since files may contain data that is not Unicode (or even text at all), files need to be able to accept raw bytes. Files are also usually buffered at some level (and so need support for flushing). It thus makes more sense to implement std::io::Write. Implementing std::fmt::Write would just introduce more ambiguity without actually letting you do more.

1 Like

fmt::Write works for &mut io::Write.

Just use a mutable reference to the file.

1 Like

That all makes sense, although I do see utility for being able to be both. The couple of times that I've bumped up against it were when I wanted a function or a struct to accept a generic writer where what is being written is ASCII, and therefore also Unicode. At that point it makes sense ergonomically to use fmt::Write because you might want to write into a String, but if you might also want to accept a File and forget about this little hiccup you can box yourself into a corner and wind up doing some rewrites. At that point if I want a String I generally just accept writing into a Vec instead l, then convert back to a String and mark any Unicode errors as unreachable.

There is another reason not to implement or use fmt::Write here: it cannot report the details of IO (or other) errors. (When you use write!() with io::Write, you do get io::Error.)

4 Likes

Now that's a good reason. Thanks.

The write!() macro is an odd case of Rust using duck typing, and achieving polymorphism with syntactic similarity instead of the type system.

I think this way of solving the problem has created a blind spot in how these traits would work together in generic contexts. In other situations having the same method name on two traits used together would be inconvenient.

If the write!() macro didn't exist, maybe there would be just one Write trait with an associated type for errors and trait bounds for supported output encodings.

4 Likes