Help understanding how trait bounds work


#1

I’m still fairly new to rust, but I’ve been fairly pleased with my progress so far, however, I’m stuck with a particular error that I have no idea where to start looking.

There’s a stripped down version below from play.rust-lang, it’s from a mini problem I’m working on where I need some conditional buffering of a file depending on the first few lines, I’m sure there is a more idiomatic way of solving my problem, but I want to understand this current error first.

I think my understanding of how traits work is faulty: from below, I’m passing around a source and rdr variable, all of which have a trait bound of implementing io::Read. The general flow is: source bytes/file is passed into EncodingDetector which implements io::Read so that it can read the first few lines of the input stream to determine what the encoding is. The encoding is set on ReadBytes, and the EncodingDetector wrapped stream is passed into it. The ReadBytes is then stored as the input stream on the SourceFile, which requires the input stream/source to also implement io::Read.

Am I missing something simple, or is my understanding of how traits work incomplete?

Thanks!

use std::io;

struct ReadBytes<R, B> {
    rdr: R,
    buf: B,
}

impl<R: io::Read> ReadBytes<R, Vec<u8>> {
    pub fn new(rdr: R) -> ReadBytes<R, Vec<u8>> {
        ReadBytes { rdr: rdr, buf: vec![] }
    }
}


impl<R: io::Read, B: AsMut<[u8]>> io::Read for ReadBytes<R,B> {
    fn read(&mut self, buf: &mut [u8]) -> io::Result<usize> {
        self.rdr.read(buf)
    }
}


struct SourceFile<R>
where
    R: io::Read,
{
    source: R,
}

impl<R> SourceFile<R>
where
    R: io::Read,
{
    fn new(source: R) -> SourceFile<R> {
        let e = EncodingDetector::new(source);
        let rb = ReadBytes::new(e);
        //XXX: ReadBytes implements io::Read, why does this complain?
        SourceFile { source: rb }
    }
}

struct EncodingDetector<R>
where
    R: io::Read,
{
    source: io::BufReader<R>,
}

impl<R> EncodingDetector<R>
where
    R: io::Read,
{
    fn new(source: R) -> EncodingDetector<R> {
        EncodingDetector { source: io::BufReader::new(source) }
    }
}

impl<R> io::Read for EncodingDetector<R>
where
    R: io::Read,
{
    fn read(&mut self, buf: &mut [u8]) -> io::Result<usize> {
        self.source.read(buf)
    }
}

fn main() {
    let source = &b"source string"[..];
    let s = SourceFile::new(source);
}

(Playground)

Errors:

   Compiling playground v0.0.1 (file:///playground)
error[E0308]: mismatched types
  --> src/main.rs:37:30
   |
37 |         SourceFile { source: rb }
   |                              ^^ expected type parameter, found struct `ReadBytes`
   |
   = note: expected type `R`
              found type `ReadBytes<EncodingDetector<R>, std::vec::Vec<u8>>`

error: aborting due to previous error

For more information about this error, try `rustc --explain E0308`.
error: Could not compile `playground`.

To learn more, run the command again with --verbose.


#2

new(source: R) -> SourceFile<R> has to return exactly SourceFile<R>, but you’re trying to create and return a SourceFile<ReadBytes<EncodingDetector<R>, Vec<u8>>>. That is, you’re trying to return with some other type that implements io::Read, but you didn’t say that in the function signature.

You could hide that in the struct definition if SourceFile always wraps R that way:

struct SourceFile<R>
where
    R: io::Read,
{
    source: ReadBytes<EncodingDetector<R>, Vec<u8>>
}

But perhaps just new does this, and you might have different constructors that wrap it a different way. If you still don’t want to be explicit about actual return type, you could hide it with impl Trait:

    fn new(source: R) -> SourceFile<impl io::Read> { ... }

#3

Just a slightly alternate take.

  1. You might want the argument to be independent of SourceFile in which case;
    fn new<S: io::Read>(source: S)
  2. This makes the function not generic over structure so leads to confusion when in generic impl struct; Could either make it a totally free function or just use some Dummy.
impl SourceFile<std::io::Empty> {
    fn new<S: io::Read>(source: S) -> SourceFile<impl io::Read> {

#4

When you write a generic argument, the caller of the function chooses the actual type. Rust complains because you’ve made the function choose the actual type, overriding function caller’s choice.

So R: io::Read does not mean any type that implements io::Read. It means one very specific type, that you don’t know about.