Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

Use fmt::Write or io::Write trait for my writable type?

Tags:

rust

I have a struct that can be converted to text through a complex series of method calls which contain numerous write! calls. This text can be written to a file or to a debug log. I am trying to decide whether to use fmt::Write or io::Write. I can't really use both because then all of the writing methods would need to be duplicated.

Simplified example:

impl MyType {
    fn write_it(&self, writer: &mut impl ???) {
//                                       ^ fmt::Write or io::Write?
        self.write_header(writer);
        self.write_contents(writer);
        self.write_footer(writer);
    }

    fn write_header(&self, writer: &mut impl ???) {
        write!(writer, "...")
    }
    // and more...
}

The docs for fmt::Write say,

...This is similar to the standard library's io::Write trait, but it is only intended for use in libcore.

So this leads me to believe I should use io::Write. This (obviously) will work well for I/O types like BufWriter. And it seems notable that serde_json does it this way.

// if I use io::Write, I can do this
my_type.write_it(&mut BufWriter::new(File::create("my-file.txt")?)?;

I also want to use my type with format! and similar macros. So I need to implement Display. In fact, isn't Display the de-facto trait for a type that can be represented as a String?

// I want to do this
println!("Contents:\n{}", my_type);

// or this
let mut s = String::new();
write!(s, "{}", my_type);

So I think I'll just tie in my implementation with Display. But here's the problem:

impl Display for MyType {
    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
        self.write_it(f)?;
//                    ^ the trait `std::io::Write` is not implemented for `std::fmt::Formatter<'_>`
    }
}

I can fix this by using impl fmt::Write, but then I lose the I/O capability mentioned before. There is no adapter between the two types (I assume there would be one if that were an intended usage). So I am caught between the two. Which trait should I use? Is there an idiomatic way to get the functionality of both?

Edit: Adapter?

@ÖmerErden suggested creating an adapter like the following. I'm just not sure what conditions will cause the UTF-8 conversion to fail. Is that conversion guaranteed to not fail as long as I write valid UTF-8 with write!? It seems too risky to me.

struct Adapter<'a> {
    f: &'a mut dyn fmt::Write,
}

impl<'a> io::Write for Adapter<'a> {
    fn write(&mut self, b: &[u8]) -> Result<usize, io::Error> {
        let s = str::from_utf8(b)
            .map_err(|_| io::Error::from(io::ErrorKind::Other))?;
        self.f.write_str(s)
            .map_err(|_| io::Error::from(io::ErrorKind::Other))?;
        Ok(b.len())
    }

    fn flush(&mut self) -> Result<(), io::Error> {
        Ok(())
    }
}
like image 640
cambunctious Avatar asked Jul 10 '20 15:07

cambunctious


1 Answers

The correct thing to do is implement Display for your type, and not have a write_it method at all. Ideally, your header, content, and footer also implement Display, because then you can write something like this:

impl Display for MyType {
    fn fmt(&self, f: &mut Formatter<'_>) -> fmt::Result {
        write!(f, "{}{}{}", self.header, self.content, self.footer)
    }    
}

However, if that's not possible, you can still write out your implementation manually like you did in your sample code.

In fact, isn't Display the de-facto trait for a type that can be represented as a String?

It's more accurate to say that Display is the de-facto trait for a type that can be written to something. Converting things to strings is just one of the many use cases for Display, but on its own it doesn't create any intermediary strings.

like image 66
Lucretiel Avatar answered Nov 17 '22 22:11

Lucretiel