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.

This topic was automatically closed 90 days after the last reply. We invite you to open a new topic if you have further questions or comments.