Accept generic type (e.g. Reader)

I'd like to create a custom stream reader (e.g. my own BufReader). My function/struct must accept any kind of stream so I can then perform stream.read(buf). One more thing, I'm using async_std.

For example:

struct StreamReader<S> {
    stream: S,
}
impl<S> StreamReader<S> {
    pub fn new(stream: S) -> Self {
        self.stream = stream;
    }
    pub async fn read(&mut self) {
        loop {
            let mut byte = [0u8];
            let size = self.stream.read(&mut byte).await; // ERROR on `.read()`
            ...
        }
    }
}
...
StreamReader::new(async_std::net::TcpStream::new...);
StreamReader::new(async_std::os::unix::net::UnixStream::new...);

Do I have to create a custom Stream type or smth?

If you don't declare that your type parameter S is a reader, then you can't count on that inside the implementation. You need to add the appropriate trait bound like impl<S: Read> StreamReader<S>. (Not sure about the actual trait name, but if it's async stuff it's probably not exactly Read – you'll need to look it up in the relevant documentation.)


To elaborate on why this is: Rust traits are a contract between the implementation and the caller. The implementation says: "If your type satisfies this trait, I can do useful things with it". E.g. if your type is Read, then the implementation can .read() from it.

Then when you "instantiate" the generic (i.e. specify a concrete type in place of the type parameter), the compiler checks whether the type you are trying to substitute implements the required trait. If not, you get a compiler error – because how would then the impl use, for example, the methods of the trait on your type, if it didn't implement the trait?

Conversely, if there was no requirement to declare trait bounds, as is the case with C++ templates, for instance, then you could again pass any type at all even if it didn't have a .read() method. This leads to a situation where you can forget that your type needs to uphold some property and you silently assume it in the code, which will be rewarded with surprise compilation errors later, for instantiations that you otherwise intended to be correct and usable.

So basically, Rust's upfront checking saves you from bad surprises, by ensuring that once a generic function typechecks, so will all of its future instantiations that you wanted to allow in the first place.

2 Likes

Thanks, @H2CO3. I'm still catching up, but it seems the right implementation looks like this:

pub async fn my_reader(stream: &mut (impl async_std::io::Read + Unpin + ?Sized))

When your type gets that complicated, I recommend translating it to generics.

pub async fn my_reader<R>(stream: &mut R)
where
    R: async_std::io::Read + Unpin + ?Sized,
{ ... }

Much shorter line length needed for that.

2 Likes

Agreed, and while we're at style, I also prefer taking readers and writers by value, since (at least for std::io), there are Read impls for &mut R (and if there aren't in other libraries, there should be, probably). And not having to take a mutable borrow all the time is pretty convenient for writing as well as reading.

1 Like

This topic was automatically closed 90 days after the last reply. New replies are no longer allowed.