Chose between trait with a generic function, a generic trait and an associated type

Hi guys,

I'm trying to generalize one of my lib which is currently using standard type like Vec<u8>.
Several possibilities, but each one has their own drawbacks.

  • trait with a generic function
trait A {
    fn func<W: Write>(&self, v: &mut W) -> Result<()>;
}

Works ok but I can't implement the trait for a specific W.

  • generic trait
trait B<W: Write> {
    fn func(&self, v: &mut W) -> Result<()>;
}

Ok but when using it on generic structs for example, need to add PhantomData which is inconvenient. E.g:

struct PointB<T, W>
where
    T: B<W>, W: Write,
{
    x: T,
    y: T,
    
    // annoying
    z: PhantomData<W>
}
  • associated type
trait C {
    type Writer;

    fn func(&self, v: &mut Self::Writer) -> Result<()>
    where
        Self::Writer: Write;
}

Could solve the former example PointB, solve a specific implementation for a writer. But not all of them.

Any hint welcomed :slight_smile:

Code here: Rust Playground

What makes you think you need to do that? Just leave the parameter off the implementing type.

struct PointB<T> {
    x: T,
    y: T,
}

And leave the bound off the trait for that matter.

trait B<W> {
    fn func(&self, v: &mut W) -> Result<()>;
}

The Write trait seems to meet your requirements, since all three options rely on it. If you don't need your own trait, can you just use Write?

Thanks for replying.

Saying that because the real usage is more complicated than that. T needs to implement B because my proc macro is relying on that behavior.

You probably won't get great feedback without explaining your actual requirements.

2 Likes

It is not the case that one is better than another. It very much depends on what you want to express with this trait. It's not possible to recommend either the generic trait, the generic method, or the associated type, without knowing what domain-specific concepts they should express.

You're both right. I was so focused on trying to solve my problem that I didn't take the time to elaborate a little bit, sorry for that !

In one of my crate which is dedicated at providing proc macros to convert structures and enums to bigendian data buffers, I have 2 traits:

/// Copy structured data to a network-order buffer. Could be used on a ```struct```, an ```enum```, but
/// not an ```union```.
pub trait ToNetworkOrder {
    /// Returns the number of bytes copied or an [`std::io::Error`] error if any.
    fn serialize_to(&self, buffer: &mut Vec<u8>) -> std::io::Result<usize>;
}

/// Copy data from a network-order buffer to structured data.
pub trait FromNetworkOrder<'a> {
    /// Copy data from a network-order buffer to structured data.
    fn deserialize_from(&mut self, buffer: &mut std::io::Cursor<&'a [u8]>) -> std::io::Result<()>;
}

Those traits are defined for primitive types (integers, floats) and some generic ones like Vec<T> etc. They extensively use the byteorder crate.

In addition, I also added 2 proc macros to auto-generate those 2 traits for user structs or enums.

Now I want to make those traits more generic, to allow not only Vec<u8> (for the first one for example), but a generic W: Write type (or R: Read for the second one). But none of the aforementioned methods make it, each having their drawbacks.

Main branch is here: GitHub - dandyvica/type2network: Traits and procedural macros to convert structures and enums to data streams

Hope this clarifies a little.

In this case, you need to decide whether:

  • you want implementors of the trait to support every possible reader/writer type. This gives callers of the method greater flexibility but will impose some constraints on the implementation. In this case, you want the "generic method" approach. This is approximately what Serde does with Serialize and Deserialize.

  • Or, you want types to explicitly state what readers/writers they support. In this case, you'd go with the "generic trait" approach and you must usually generate all impls for all concrete types one by one, but in exchange, the implementations are allowed to rely on properties specific to the individual reader/writer type.

    I'm not getting where you think the requirement to add PhantomData comes from in this case. Just don't put any trait bounds on generic UDTs, period. With a very few exceptions (when you need an associated type as a field), bounds belong on impls, not on type definitions.


My hunch is that if you are currently trying to generalize something, you should probably go with a generic method. The "one impl per writer type" largely defeats the purpose IMO; and relying on specifics of reader/writer types is rarely needed (or the right thing to do) anyway.

1 Like

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.