Does this type parameter matter and can I omit it?

Consider this code:

use std::io::Write;

struct Writer<W: Write> {
    inner: W,
}

impl<W: Write> Writer<W> {
    fn new(inner: W) -> Self {
        Writer { inner }
    }
    fn write_something(&mut self) {
        self.inner.write(b"something").unwrap();
    }
}

fn main() {
    let buf = Vec::new();
    let writer = Writer::new(buf);
    takes_a_writer(writer);
}

fn takes_a_writer(mut writer: Writer<Vec<u8>>) {
    writer.write_something();
}

My question is about function takes_a_writer(). To make this function take a Writer as an argument, I have to specify the actual inner type of that Writer (Vec<u8>), even though it would make no difference. (Or would it?)

In this example it's easy to just add a type parameter to takes_a_writer(), but if Writer contains many such inner types, that means every function that takes such a struct has to have all those type parameters as well. Is there a better way?

The compiler needs to know what memory layout the argument will have in order to complie. If you want to define a set of functions that each takes a different kind of Writer, that’s what generics are for:

fn takes_a_writer<W>(mut writer: Writer<W>) {
    writer.write_something();
}

If all the methods you need come from a trait, there’s also this syntax:

trait Write { /* ... */ }

fn takes_a_writer(mut writer: impl Write) {
    writer.write_something();
}

Another option:

fn takes_a_writer(mut writer: Writer<impl Write>) {
    writer.write_something();
}

It's still generic, but without naming an explicit W type parameter.

2 Likes

Oh, in the case where Writer has just this one inner, this actually makes it nicer. But when there are more type parameters, I think keeping track of their order quickly gets confusing.

Now I realize that maybe this is what I was going for with my question... an obvious solution is to define a trait for my Writer:

use std::io::Write;

struct Writer<W: Write> {
    inner: W,
}
trait WriterTrait {
    fn write_something(&mut self);
}
impl<W: Write> Writer<W> {
    fn new(inner: W) -> Self {
        Writer { inner }
    }
}
impl<W: Write> WriterTrait for Writer<W> {
    fn write_something(&mut self) {
        self.inner.write(b"something").unwrap();
    }
}

fn main() {
    let buf = Vec::new();
    let writer = Writer::new(buf);
    takes_a_writer(writer);
}

fn takes_a_writer(mut writer: impl WriterTrait) {
    writer.write_something();
}

This seems like a common need so maybe I was hoping there's some shortcut (syntactic sugar) for it.

I don't think I would consider that trait approach common. I certainly would have written this:

fn takes_a_writer<W: Write>(mut writer: Writer<W>) {
    writer.write_something();
}
1 Like

Alternative: extension trait.

use std::io::Write;

trait WriteExt: Write {
    fn write_something(&mut self);
}

impl<W: Write> WriteExt for W {
    fn write_something(&mut self) {
        self.write(b"something").unwrap();
    }
}

fn main() {
    let mut buf = Vec::new();
    takes_a_writer(&mut buf);
}

fn takes_a_writer(mut writer: impl Write) {
    writer.write_something();
}

playground

1 Like

I mean... how many parameters are we talking about here? Three? Five? Ten?

If takes_a_writer should, in fact, accept any Writer<W>, then the sensible thing to do is add W as a type parameter. Using impl Write can make that slightly cleaner. But if you're reluctant to add another type parameter because Writer already has 10, well... a struct with that many inner types that are all generic is probably already trying to pull together too many unrelated things, and should be split up or something.