How to create BufReader<_> from Option<File> with std::io::stdout() as fallback in a Rust way?


#1

Hello there,

As title says, I have Option<File> and want to get some BufWriter<_> out of it, falling back to std::io::stdout() if no file is present.

Right now I doing it this way:

let fo: Option<File> = ...
let mut ow = BufWriter::new(match fo {
    Some(f) => Box::new(f) as Box<Write>,
    None => Box::new(io::stdout()),
});

and thing that puzzles me is why I need as Box<Write> or ow: BufWriter<Box<Write>> hints.

I (kinda) get it why I cannot just return BufWriter<File> and BufWriter<Stdout> from match branches because of monomorphization (right?!), but what I dont get is:

If Box<Write> is Write and Box<Stdout> is Box<Write> after Box<File> is Box<Write>, why cannot compiler infer that nearest thing to W: Write for BufWriter from Box<File> and Box<Stdout> is to ‘cast’ them both to Box<Write>?

The next puzziling thing is why I can drop as Box<Write> if I set ow: BufWriter<Box<Write>> type explicitly, but I cannot move as Box<Write> after match due to ‘match arms have incompatible types’ error.

And last thing – maybe I’m doing it all wrong from the start and there is a better way to achieve this?


#2

I usually do this in two steps (playground).

let writer: Box<Write> = match foo {
    Some(file) => Box::new(file),
    None => Box::new(io::stdout()),
};

let buffered = BufWriter::new(writer);

That way you just need the first Box<Write> to tell the compiler to make both arms of the match statement trait objects.

Your problem is that you need to tell the BufWriter what type of writer it’s containing, otherwise type inference thinks the arms of your match statement are incompatible. I’m pretty sure this is because the compiler wants you to make it explicit when you want it to create trait objects.


#3

An alternative is to define your own enum with 2 variants, one for holding a File and another Stdout. Impl Write for it by delegating to the underlying value. Then you can skip the boxes and trait objects altogether. Whether this is better depends on context.


#4

Whoa, thanks for the insight. Context is simple: I have a small script that reads and parses file by filename given by arguments and then writes its results into another file or stdout, if no file was given in args (or args contained '-'). Like that: cmd test.txt -.

One more question then: what is more ‘Rust way’ – try to open output for writing right after program start or be lazy and try to do so just after some results are ready?


#5

Not sure there’s a “Rusty” (or not) way of doing this - it mostly comes down to user experience, I think. If, for example, it takes a long time to get results (doesn’t sound like it in your case, but let’s imagine it so), you may want to open the file early to detect any issues that you may encounter in doing that - that way the user doesn’t wait around for a while only to learn that they cannot write the results to a file because, e.g., of a permissions issue.

If the results are available quickly, I don’t think it matters much.


#6

Sorry, I didn’t address the “context” point. By context, I really meant whether the heap allocations (due to boxing) matter - they really don’t given your description. So, an enum, while elegant and memory frugal, is overkill and would entail a bit of boilerplate for no real gain. Stick to the boxes :slight_smile:


#7

Firstly, thanks for all the help.

I have another slightly connected question: how can I make second version of get_writer compile successfully (playground)?

fn get_writer1<P: AsRef<Path>>(path: P) -> Result<BufWriter<File>, Box<Error>> {
    let f = File::create(path)?;
    Ok(BufWriter::new(f))
}

fn get_writer2<P: AsRef<Path>>(path: P) -> Result<BufWriter<File>, Box<Error>> {
    File::create(path).map(BufWriter::new)
}

I’ve tried to .map_err(Box::new) and such but :frowning:

Also, maybe it’s better to return std::io::Error here instead of std::error:Error?


#8

https://play.rust-lang.org/?gist=94fe6aee393a90f27793bb6bb40a286a&version=stable

Yeah, map_err is the way to go. I’d return std::io::Error as well here, and then you don’t need the box or the map_err.