Implementing a large number of distinct but similar structs

Suppose I have a family of structs which are identical data structures, but each of which implements a large number (let’s say dozens) of traits many of which they have in common. I would nevertheless like for these structs to be distinct, in other words, no two structs have exactly the same set of traits (obviously, otherwise they'd be entirely equivalent).

For the sake of making this a little more concrete, what I have is something like

struct FourFloatVec1 {
    data: [f64; 4],
}

What I need is lots of inequivalent versions of this with different mathematical properties (e.g. some are true vectors and can be added and subtracted, some are merely coordinates, some transform under different representations of different inequivalent mathematical groups, et cetera), but which have many properties in common.

There's nothing stopping me from just doing this explicitly, but it seems like it will require a staggering amount of boilerplate.

What is the recommended way to deal with this? I think you are going to tell me that I should make a custom derive macro, which currently seems like the only plausible option to me, but I thought it was worth checking what experienced people have to say about it.

If the expected structs don't change much, I might opt for manually declaring each struct, and a declarative macro for the impls.

The reasons:

  • IDEs can find regular struct declarations easier, and might not be able to find macro generated types.
  • Saves on compilation time – one less syn quote proc-macro2 linked binary.

It's a maintenance trade off – sometimes I do opt for a proc macro crate because I know things may change, and code is much easier than mass text replace.

I still go with declaring struct Something; (my IDE can see the symbol), then get the proc macro to generate the other derives and impls

2 Likes

Yeah, definitely no objection to explicitly declaring the structs, it's the trait impls that I'm worried about, that's the part that would wind up being many hundreds, maybe thousands of lines of boilerplate.

How complex - and particularly, how distinct - are the actual impls?

It might be feasible to implement your own derive macro, so that the common cases can be reduced to #[derive(TraitA, TraitB, TraitC, …)], but only if the impls are fundamentally very similar and mechanical. The more flexibility you need, the more complex that derive macro becomes, and the less you gain over implementing the traits individually.

For some motivating examples, I’d suggest looking at thiserror, which implements a derive macro for the Error trait.

A useful middle ground between macro_rules! and writing a derive proc-macro is macro_rules_attribute, which allows you to use a macro_rules! macro as if it were a derive macro or attribute macro. This way, your macros can be informed by the struct definition (without having to put the struct inside my_macro! {}), but you don't have to write proc-macro code, which makes it more feasible to have lots of individual macros to handle the individual things that some but not all of your types have in common.

That said, it might be that macro_rules! itself is all you need. Here are several examples of vector-math impls defined using macros:

These macros aren’t trying to produce useful results for a wide variety of struct declarations, like a typical derive macro; rather, they have to be used in extremely specific ways — but that is all you need to greatly reduce boilerplate.

5 Likes

If it is not practicable to write some macros with some helper functions, maybe just write a small tool to generate the source code.

In most cases they are identical, since the structs are identical data structures.

This is not entirely unlike what I have in mind.

Ok, so it sounds like probably one way or another the canonical rust way of doing this is macros, albeit whatever mix of macro types is most appropriate for each use case.

You could add generic marker types for each capability.

So you have a type CanAdd and CantAdd
And your struct is Data<TAddability, TMultiplicability> etc where each generic parameter is an orthogonal capability

The each impl just only impl for the relevant marker types

I.e. Impl<T> Add for Data<CanAdd, T>

The you create a number of type aliases for relevant combinations

On second thougt you can also use just one generic parameter

So you have the marker AddAndMultiply which impls a trait CanAddTrait and CanMultiplyTrait and blanket impl<T> Add for Data<T> where T: CanAdd

In std (or more accurately: core), the functions on the primitive integer types are defined using macros, to reduce the boilerplate between u8, u16, u32, i8 etc.

So, yes. Using custom module-local (or crate local) macro rules is quite idiomatic I would say.

How would this work? T needs to be constrained to a type that supports addition, otherwise you won't be able to write the impl.

I have no idea but isn’t adding PhantoData fields and to otherwise identical structures and generics helpful?

As op stated each of the types has the same internal structure. So they only differ by capability. The T in this example was the TMultiplicability which is ignored for add

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.