Mutate generic structs

Hi, I'm pretty new to Rust and I need to mutate a struct in a place where I do not know which struct I want to mutate. I come up with the below solution:

#[derive(Debug)]
pub struct ModifyError {}

type R<X> = Result<X, ModifyError>;

pub trait GeneralField {}

pub trait Mutable {
    type Field: GeneralField;

    fn modify(self, field: Self::Field, value: String) -> R<Self>
    where
        Self: std::marker::Sized;
}

#[derive(Debug, Clone)]
pub struct Astruct {
    text: String,
}

pub enum AstructField {
    Text,
}
impl GeneralField for AstructField {}

impl Mutable for Astruct {
    type Field = AstructField;

    fn modify(self, field: Self::Field, value: String) -> R<Self> {
        match field {
            Self::Field::Text => Ok(Astruct {
                text: value,
                ..self
            }),
        }
    }
}

//fn general_modify<T: Mutable, X: GeneralField>(to_modify: T, field: X, value: String) -> R<T> {
//    let new = to_modify.modify(field, value);
//    new
//}

fn general_modify<T: Mutable>(to_modify: T, field: <T as Mutable>::Field, value: String) -> R<T> {
    let new = to_modify.modify(field, value);
    new
}

fn main() {
    let unmodified = Astruct {
        text: "Ciao".to_string(),
    };
    let modified = general_modify(unmodified.clone(), AstructField::Text, "Hello".to_string());
    println!("Unmodified struct is: {:#?}", unmodified);
    println!("Modified struct is: {:#?}", modified);
}

I have some questions:

  1. The above approach make sense?
  2. Is there a standard way to achieve my purpose? Maybe use Serde?
  3. The commented function do not work, I think that is because with the function signature I could pass something that implement the GeneralField trait but is not the specific type that my specific Mutable struct need and so the compiler prevent me to make an error, and that is wonderful. What I'm wondering is at this point I really need to define the GeneralField trait? Also I can not really understand the syntax <T as Mutable>::Field where can I find some documentation about that? To me it doesn't make a lot of sense because field in the specific above case is AstructField and do not implement Mutable so in the signature it seems that T is both something implementing Mutable and something that is used inside Mutable as Field, is that correct?

Well, your commented out method takes a to_modify which is of type T, and a X: GeneralField, but the method you are calling on to_modify only works for field: Self::Field which is defined in the trait Mutable.

What if I where to call your function with a random type that implements GeneralField. Look at the implementation of Mutable for Astruct. It only works with one specific type. So the compiler won't let you call modify with types it can't take, and your function signature (of general_modify) obviously takes types it can't take.

<T as Mutable>::Field is a way to refer to the associated type in a specific impl for Mutable. I don't know if there is any detailed write up about it... It basically says consider T as a type that implements Mutable, now look for Field.

Imagine 2 traits have an associated type or a method that has the same name. If you where to say Astruct::Field, which one are you refering to, the one from trait X or Y? Normally you only need to put it when there is possible ambiguity, at least for methods.

The above approach make sense?

That's hard to tell. I'd say you don't need the GeneralField trait unless you want to be able to call methods on the field without knowing it's type, but the trait impl for Mutable will know the exact type, so you probably don't need it.

Rust is a type strict language, which has a certain conveniences/inconveniences and which invites certain designs more than others. Here you seem to want to work on an object of which you don't know the type to change a field of which you don't know the type. That is quite uncommon and pushing the type system a bit, so it might not be the most idiomatic design, but it's possible and maybe there is no better solution to what you are trying to solve, but that's impossible to tell without more context.

I suppose you can do this, but it's a bit clunky compared to just making a trait for the specific kind of field you wish to modify.

trait HasTextField {
    fn get_text(&self) -> &str;
    fn get_text_mut(&mut self) -> &mut String;
    fn set_text(&mut self, value: String);
}

The issue is exactly as you have noticed that it would allow the caller to use any kind of field as long as it implements GeneralField, but T::modify only accepts values of type T::Field, which in the case of T = Astruct is AstructField.

As for the GeneralField trait, you are correct in that you can simply remove the trait.

In this case Field is called an associated type, and you can read more at the link. In this case we have

  1. T = Astruct
  2. so T implements Mutable
  3. thus T has an associated type called Field
  4. which in this case is the type AstructField

Note that in this case the simpler version T::Field would also compile. It's basically a way of asking for the Field type associated with T's implementation of the Mutable trait. The <T as Mutable>::Field thing is a way to specify that we want the Field from the impl of the Mutable trait, and is used to disambiguate the case where T implements several different traits that all supply an associated type by the name Field.

Ok very clear. Thanks for the answers. I'll delete the trait GeneralField and I stay with the implementation above. Doing a trait for the specific field that I want to modify I think that can not work cause I need to mutate a generic type with generics fields and as input I have a String so I need to implement the serialization logic for every type that I want to modify. Ty again.

1 Like

This topic was automatically closed 90 days after the last reply. New replies are no longer allowed.