Field access in trait

Playground: Rust Playground
I want to implement a generic function with a large number of concrete versions, and access fields of each struct within each concrete version. I can't find a way to express this.

trait Message {
    fn send<T>(msg: T);
}

#[derive(Debug)]
struct Green { f1: u32 }

#[derive(Debug)]
struct Red { f2: u8 }

impl Message for Green {
    fn send<T: core::fmt::Debug>(msg: T) {
        println!("Green: {:?}", msg);
    }
}

impl Message for Red {
    fn send<T: core::fmt::Debug>(msg: T) {
        println!("Red: {:?}, f2: {}", msg, msg.f2);
    }
}

This fails in Red, because T is generic and doesn't have a field f2. "Green" is fine, because it doesn't do anything Green-specific.

fn send<T: core::fmt::Debug>(msg: Red)

doesn't match the trait.

fn send(msg: Red)

is rejected for not having a generic parameter.

Hm. How do you do this?

Is T always the implementer? You could make the trait

trait Message {
    fn send(self);
}

In the real program, "self" is already in use. Actual usage is more like

channel.send(msgred)

where there's only one type of channel but lots of message types.
This needs a generic parameter.

To reply to your immediate question, you could add the declaration of an accessor to another trait itself, like this:

trait HasField2 {
    fn get_field2(&self) -> u8;
}

and impose this bound on the msg parameter:

trait Message {
    fn send<T: HasField2>(msg: T);
}

finally, call this accessor in impl Message for Red.


However, this makes a more fundamental problem apparent. You couldn't implement HasField2 for Green. The underlying issue is that you are expecting different kinds of behavior and "contracts" in both implementations of Message. In other words, your two implementations of Message shouldn't probably be impls of the same trait. There's a design issue that only you can resolve using the knowledge of the non-toy-example problem you have in mind.


It also makes little sense to me why the Message trait has an argument named msg. Isn't the message self? And if you are trying to do channel.send(message);, then are you implementing Message for a channel, rather than a message? Aren't you indeed confusing self and the type parameter?

It looks like you are actually trying to write something like a visitor. In that setting you would flip the traits around, and you would have a Channel trait which can send primitive messages, and then implementations of Message would rely on Channel while also being aware of their own, concrete type (and hence, fields). Something like this:

trait Channel {
    fn send_bytes(&self, bytes: &[u8]);
}

trait Message {
    fn send_to<C: Channel>(&self, channel: &C);
}

impl Message for Red {
    fn send_to<C: Channel>(&self, channel: &C) {
        let string = format!("Red: {:?}, f2: {}", self, self.f2);
        channel.send_bytes(string.as_bytes());
    }
}

impl Message for Green {
    fn send_to<C: Channel>(&self, channel: &C) {
        let string = format!("Green: {:?}", self);
        channel.send_bytes(string.as_bytes());
    }
}
2 Likes

OK. I get it now. I knew that Rust generics were much weaker than C++ generics, but I hadn't realized how much weaker. There's no such thing in Rust as defining a generic and then providing concrete implementations of it for different types. There is only the "trait" system, and that only works on "self": "A trait is a collection of methods defined for an unknown type: Self"

I'm not sure I get what you are trying to get at here. Putting aside the language rant, I don't understand what you mean by "defining a generic". And what do you mean "providing concrete implementation of it for different types"? For sure, you can implement a trait for multiple different types. It's just that items in traits can refer to functions and consts, not fields, because there is no good and unambiguous way to define a field in a trait, since field definitions are part of the data and the layout of the type.

If your complaint is that Rust's traits aren't based on raw text substitution like C++ templates, then I have to disappoint you: that is a deliberate decision, and not a limitation, because it removes a huge surprise factor from the language. You can only use what you declare, therefore the implementation of a trait for a specific type won't cease to compile due to spooky action-at-a-distance.

There's a postponed RFC for "fields in traits", and I'm pretty sure I've seen Niko express an ongoing interest more recently than that RFC. (Who knows when it might be revisited; edition 2021 is looking like a "no large changes that aren't already in progress" sort of cycle.)