Procedural Macros to generate method for struct with complex types

I am working on a project that has several structs that hold related information (think objects). Most of the types in these structs are basic types such as String, f64, etc, but I am using types from the Dimensioned package and also Rc<RefCell<>> types. There are also Vec and HashMap fields.

In a library function, I read in data from files using serde then reconstruct that into my proper types along with reference counted references to other structs (I have datatypes that can either be a type or instance of a type(think of a struct that holds info common to a specific model of laptop, and then another one that hold info common to a single laptop, that might have a reference to one of several different laptop models), and several instances can refer to one type). During this reconstruction, there might be 2 instances of a defined instance or type read in from different datafiles, and I want to be able to prompt a user to merge these 2 instances at the end of parsing and reconstruction.

Currently, I have defined a method on each type that compares the fields of two of the same type, prompts the user to perform a manual merge between them, and then does the actual merge logic. Since these methods vary in length, depending on the total number of fields in the structs, these functions get tedious to manually write, and keep updated if I add a new field to the structs.

I decided to mitigate this tedium by writing a derive proc-macro that generates these methods for me, but am running into issues with the complex types, specifically that I need to special case the Vec and HashMap types so I can string their contents together, rather than just using display() or to_string(). I cannot figure out how to do this using syn. I am also having weird issues with Option<> and the dimensioned types, where I get errors complaning that you cannot chain comparison symbols during cargo --expand

I can't just compare 2 objects directly, since they may have the same ID, but different contents unfortunately.

i would appreciate assistance with either/or:

  • a solution for special casing the troublesome types in my proc-macro
  • a solution for this merging process that wouldn't rely on proc-macros or manually writing long boilerplate macros that are tricky to update.

Source code is on github here.

Thank you

Instead of special-casing types, you could try to special-case fields. Have your proc-macro look for a #[special_snowflake_case] attribute on each field, and change its behavior if it finds one.

1 Like

Procedural macros tend to go hand-in-hand with traits. A common situation is you'll add a #[derive(MyTrait)] to a struct and that'll generate code which implements MyTrait by recursively calling into the MyTrait implementation on each of its fields. That way instead of trying to figure out what type a field has and generating custom logic for it, you just call a method and let the type's implementation of your trait decide for itself.

You could imagine creating some sort of Merge trait and implementing it for String, f64, and so on, as well as Vec<T> where T: Merge and HashMap<K, V> with their custom logic.

I don't know your exact use case so I'll be making things up as I go, but it might look something like this:

trait Merge {
  fn merge(self, other: Self, ctx: &mut Context) -> Result<Self, Error>;
}

impl Merge for String {
  fn merge(self, other: Self, ctx: &mut Context) -> Result<Self, Error> {
    if self == other {
      return Ok(self);
    }

    let desired_value = ctx.select("Which one do you want to use?", self, other)?;

    Ok(desired_value)
  }
}

Composite values would implement Merge by just calling Merge::merge() for each of its fields.

I've also introduced a Context helper which will have various methods that you may want to reuse. For example,

struct Context { ... }

impl Context {
  /// Display a select prompt, returning the value that was selected.
  fn select<T: Display>(&mut self, prompt: &str, first: T, second: T) -> Result<T, Error>  {
    ...
  }
}

You could also add things like pools of existing objects to the Context so you can reuse existing laptop models or maybe print some sort of "path" so they know what fields they are modifying, or whatever.

The dialoguer crate should also be really useful for things like recieving input or asking the user to select options from the command-line.

For extra flexibility, crates like serde and clap typically allow you to put an attribute on a field so you'll call some custom function instead of going through the normal trait implementation for that type. You could imagine adding a #[merge(with_fn = path::to::merge)] attribute above a particular field to call that function for merging instead of Merge::merge().

3 Likes

Thank you. This is pretty much the pattern I am attempting right now. Can you provide an example of the composite values, and how to construct the implementation for the entire struct? No need for the proc-macro wizardry, just an extended example of what you have already!

The complicated part around prompting a user, is I want the library to stand on its own and not specify an implementation so you can map it to any form of UI you can program.

thank you. This definitely could be useful, but it would still be annoying to special case all the different types. I would probably wind up with at least 12 different attributes to keep track of.

I would make my proc-macro generate something like this:

#[derive(Merge)]
struct Person {
  name: String,
  address: Option<Address>,
}

#[derive(Merge)]
struct Address {
  street: String,
  country: String,
}

impl Merge for Person {
  fn merge(self, other: Person, ctx: &mut Context) -> Result<Self, Error> {
    let name = self.name.merge(other.street, ctx)?;
    let address = self.address.merge(other.address, ctx)?;
    Ok(Person { name, address })
  }
}

impl Merge for Address {
  fn merge(self, other: Person, ctx: &mut Context) -> Result<Self, Error> {
    let street = self.street.merge(other.street, ctx)?;
    let country = self.country.merge(other.country, ctx)?;
    Ok(Address { street, country })
}

To make things work nicely we'll also want to implement Merge for Option<T>.


impl<T: Merge> Merge for Option<T> {
  fn merge(self, other: Option<T>, ctx: &mut Context) -> Result<Self, Error> {
    match (self, other) {
      (Some(left), Some(right)) => {
        let merged = left.merge(right, ctx)?;
        Ok(Some(merged))
      }
      (None, None) => Ok(None),
      (Some(value), None) | (None, Some(value)) => {
        todo!("Ask if we want to keep the value or use None");
      }
    }
  }
}

To help reduce ambiguity, you might want to make the Context do the recursive call so it can add some sort of field name to a stack and print a more human-friendly message. For example, you might replace self.name.merge(other.name) with ctx.merge("country", self.country, other.country) so calling a ctx.select() helper will print something useful like "Which 'person.address.country' do you want to use?".

That's fine. Just use a trait for your context object instead of a concrete Context struct.

3 Likes

One other follow up question, can you provide a bit more info about contexts. I tried googling a bit but didn't get any concrete info that applied here.

All I have been doing thus far is passing through a function parameter which worked pretty well.

Thanks again for your help with this.

That Context object isn't a specific type from a library or design pattern, so you won't find any specific documentation or tutorials about it on the internet.

You'll probably want to share certain data and methods between parent and child objects (e.g. that name stack I was talking about) as well as make things like user prompts reuse the same code. By adding some sort of Context object to your trait's method, it provides the caller with a place to put those sort of things so they can be accessed by an implementation.

1 Like

Thanks again

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.