How to do polymorphic IO from either a File or stdin in Rust?
Asked Answered
P

3

37

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.

Porbeagle answered 18/3, 2016 at 15:20 Comment(3)
Your current approach will not work since StdinLock contains a reference to a Stdin object.Rebba
Could you expand on that a little if you have time? Thanks.Porbeagle
See also: Forcing BufRead trait compatibility between io::stdio and BufReaderApposite
R
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);
}
Rebba answered 18/3, 2016 at 16:12 Comment(0)
M
55

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.

Mcswain answered 22/4, 2018 at 8:35 Comment(1)
You should even be able to take advantage of On-Stack Dynamic Dispatch here, but that may go directly against your readability goal.Parceling
R
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);
}
Rebba answered 18/3, 2016 at 16:12 Comment(0)
F
2

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.

Footstall answered 30/12, 2022 at 14:16 Comment(0)

© 2022 - 2024 — McMap. All rights reserved.