39

I'm trying to implement a "polymorphic" Input enum which hides whether we're reading from a file or from a stdin. More concretely, I'm trying build an enum that will have a lines method that will in turn "delegate" that call to either a File wrapped into a BufReader or to a StdInLock (both of which have the lines() method).

Here's the enum:

enum Input<'a> { Console(std::io::StdinLock<'a>), File(std::io::BufReader<std::fs::File>) } 

I have three methods:

  • from_arg for deciding whether we're reading from a file or from a stdin by checking whether an argument (filename) was provided,
  • file for wrapping a file with a BufReader,
  • console for locking the stdin.

The implementation:

impl<'a> Input<'a> { fn console() -> Input<'a> { Input::Console(io::stdin().lock()) } fn file(path: String) -> io::Result<Input<'a>> { match File::open(path) { Ok(file) => Ok(Input::File(std::io::BufReader::new(file))), Err(_) => panic!("kita"), } } fn from_arg(arg: Option<String>) -> io::Result<Input<'a>> { Ok(match arg { None => Input::console(), Some(path) => try!(Input::file(path)), }) } } 

As far as I understand, I have to implement both BufRead and Read traits for this to work. This is my attempt:

impl<'a> io::Read for Input<'a> { fn read(&mut self, buf: &mut [u8]) -> io::Result<usize> { match *self { Input::Console(ref mut c) => c.read(buf), Input::File(ref mut f) => f.read(buf), } } } impl<'a> io::BufRead for Input<'a> { fn lines(self) -> Lines<Self> { match self { Input::Console(ref c) => c.lines(), Input::File(ref f) => f.lines(), } } fn consume(&mut self, amt: usize) { match *self { Input::Console(ref mut c) => c.consume(amt), Input::File(ref mut f) => f.consume(amt), } } fn fill_buf(&mut self) -> io::Result<&[u8]> { match *self { Input::Console(ref mut c) => c.fill_buf(), Input::File(ref mut f) => f.fill_buf(), } } } 

Finally, the invocation:

fn load_input<'a>() -> io::Result<Input<'a>> { Ok(try!(Input::from_arg(env::args().skip(1).next()))) } fn main() { let mut input = match load_input() { Ok(input) => input, Err(error) => panic!("Failed: {}", error), }; for line in input.lines() { /* do stuff */ } } 

Complete example in the playground

The compiler tells me that I'm pattern matching wrongly and that I have mismatched types:

error[E0308]: match arms have incompatible types --> src/main.rs:41:9 | 41 | / match self { 42 | | Input::Console(ref c) => c.lines(), | | --------- match arm with an incompatible type 43 | | Input::File(ref f) => f.lines(), 44 | | } | |_________^ expected enum `Input`, found struct `std::io::StdinLock` | = note: expected type `std::io::Lines<Input<'a>>` found type `std::io::Lines<std::io::StdinLock<'_>>` 

I tried to satisfy it with:

match self { Input::Console(std::io::StdinLock(ref c)) => c.lines(), Input::File(std::io::BufReader(ref f)) => f.lines(), } 

... but that doesn't work either.

I'm really out of my depth here, it seems.

3
  • Your current approach will not work since StdinLock contains a reference to a Stdin object. Commented Mar 18, 2016 at 15:41
  • 1
    Could you expand on that a little if you have time? Thanks. Commented Mar 18, 2016 at 15:55
  • See also: Forcing BufRead trait compatibility between io::stdio and BufReader Commented May 2, 2021 at 5:25

3 Answers 3

56

The answer by @A.B. is correct, but it tries to conform to OP's original program structure. I want to have a more readable alternative for newcomers who stumble upon this question (just like I did).

use std::env; use std::fs; use std::io::{self, BufReader, BufRead}; fn main() { let input = env::args().nth(1); let reader: Box<dyn BufRead> = match input { None => Box::new(BufReader::new(io::stdin())), Some(filename) => Box::new(BufReader::new(fs::File::open(filename).unwrap())) }; for line in reader.lines() { println!("{:?}", line); } } 

See the discussion in reddit from which I borrowed the code.

Note the dyn keyword before boxed BufRead. This pattern is called a trait object.

Sign up to request clarification or add additional context in comments.

1 Comment

You should even be able to take advantage of On-Stack Dynamic Dispatch here, but that may go directly against your readability goal.
21

This is the simplest solution but will borrow and lock Stdin.

use std::fs::File; use std::io::{self, BufRead, Read}; struct Input<'a> { source: Box<BufRead + 'a>, } impl<'a> Input<'a> { fn console(stdin: &'a io::Stdin) -> Input<'a> { Input { source: Box::new(stdin.lock()), } } fn file(path: &str) -> io::Result<Input<'a>> { File::open(path).map(|file| Input { source: Box::new(io::BufReader::new(file)), }) } } impl<'a> Read for Input<'a> { fn read(&mut self, buf: &mut [u8]) -> io::Result<usize> { self.source.read(buf) } } impl<'a> BufRead for Input<'a> { fn fill_buf(&mut self) -> io::Result<&[u8]> { self.source.fill_buf() } fn consume(&mut self, amt: usize) { self.source.consume(amt); } } 

Due to default trait methods, Read and BufRead are fully implemented for Input. So you can call lines on Input.

let input = Input::file("foo.txt").unwrap(); for line in input.lines() { println!("input line: {:?}", line); } 

Comments

3

If you're willing to restructure you're code a bit, you can actually get away without doing dynamic dispatch. You just need to make sure whatever code is using the reader is wrapped in it's own function and the concrete types of the arguments for that function are known at compile time.

So if we eschew the enum Input idea for a moment, and building on @Yerke's answer, we can do:

use std::env; use std::fs; use std::io::{BufRead, BufReader, Read}; fn main() { let input = env::args().nth(1); match input { Some(filename) => output_lines(fs::File::open(filename).unwrap()), None => output_lines(std::io::stdin()), }; } fn output_lines<R: Read>(reader: R) { let buffer = BufReader::new(reader); for line in buffer.lines() { println!("{:?}", line); } } 

Because we have a concrete type for R each time we call output_lines, the compiler can monomorphize the output_lines function and do static dispatch. In addition to being less complicated code in my opinion (no need for Box wrapping), it's also slightly faster and the compiler can do more optimizations.

Comments

Start asking to get answers

Find the answer to your question by asking.

Ask question

Explore related questions

See similar questions with these tags.