std::io::Write and std::fmt::Write interopability

I'm wondering if there's a better or preferred way to do the following. I want to use a library which accepts a std::fmt::Write, but in the context I have, I only have a type which implements std::io::Write.

I've just decided to buffer everything to a string first then write it to the file I want it written to. Is there a better way to interoperate between the std::io::Write and std::fmt::Write traits?

Would it have made more sense for the library to accept std::io::Write in the first place?

fn main() {
    stats(get_outfile(None)).unwrap()
}

fn get_outfile(output_filename: Option<&std::path::PathBuf>) -> Box<dyn std::io::Write> {
    match output_filename {
        Some(filename) => Box::new(std::io::BufWriter::new(
            std::fs::File::create(filename).unwrap_or_else(|err| {
                panic!(
                    "Error: {err}. Unable to open {}",
                    filename.to_string_lossy()
                )
            }),
        )),
        None => Box::new(std::io::stdout().lock()),
    }
}

fn stats(mut out: impl std::io::Write) -> std::io::Result<()> {
    let mut registry = prometheus_client::registry::Registry::default();
    let guage = prometheus_client::metrics::gauge::Gauge::<i64>::default();
    guage.set(0i64);
    registry.register("foo", "foo", guage);

    let mut s = String::new();
    prometheus_client::encoding::text::encode(&mut s, &registry).map_err(std::io::Error::other)?;
    write!(&mut out, "{}", s)?;

    Ok(())
}

If big string is of no issue when you test it, then it isn't worth writing a dozen lines, to make a structure, that's composed of io, implement fmt.

My personal opinion on this matter is that std::fmt::Write should very rarely be used explicitly; its primary job is to be implemented by things that want to be written to using formatting traits (std::fmt::Display and such).

The standard library provided way to interoperate between fmt and io here is std::io::Write::write_fmt(), which lets you write anything formattable to an io::Write stream. So, you should create a Displayable adapter type and you get the trait adaptation for free:

use std::fmt;
use std::io::Write as _;

fn function_that_demands_fmt_write(out: &mut dyn fmt::Write) -> fmt::Result {
    out.write_str("hello")
}

struct Adapter {}

impl fmt::Display for Adapter {
    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
        // fmt::Formatter implements fmt::Write
        function_that_demands_fmt_write(f)
    }
}

fn main() {
    let a = Adapter {};
    // write! calls .write_fmt()
    write!(std::io::stdout(), "{a}").unwrap();
}

Would it have made more sense for the library to accept std::io::Write in the first place?

Not necessarily, because io::Write has the significant disadvantage of not being usable in no_std environments. and also isn't implemented by String (because io::Write allows writing arbitrary non-UTF-8 bytes which String cannot accept). In my opinion, the library should instead be returning impl fmt::Display.

The exception is if the library function is intended to be called by the implementations of formatting traits like impl fmt::Display for ....

4 Likes