Return slice of struct fields

I have a struct that contains several fields which I would like to return in different combinations. e.g.

struct ABC {
  a: String,
  b: PathBuf,
  c: HashSet,
}

struct AB {
  a: String,
  b: PathBuf,
}

struct AC {
  a: String,
  c: HashSet,
}

Ideally I would like to have functions like

impl ABC {
  pub fn as_ab(&self) -> &AB {...}
  pub fn as_ac(&self) -> &AC {...}
}

but the implementation of these functions has eluded me after many attempts. For example, the most naive of these would be

pub fn as_ab(&self) -> &AB {
  &AB {
    a: self.a,
    b: self.b
  }
}

Is there a way to accomplish this? If not, what is a better way to architect things?

The exact thing you are asking for is impossible because the relative layout of the fields differs between the structs: in particular the relative position in memory of the a and c fields (likely[1]) differs between ABC and AC. Of course, this code is obviously either artificial or some sort of simplification or a real use-case, and presumably supposed to serve some purpose, i.e. aids with solving some underlying problem. If you tell us more about the context, we can discuss what alternative approaches that are possible you could take.


  1. assuming a straightforward layout, though to be precise, the exact layout for ordinary structs in Rust is not really well-defined at all, and one would also need to use things like e.g. repr(C) to reason about actual specific layouts of types ↩︎

2 Likes

The more complete context is that I have a struct that I want to save across multiple files when persisting to disk, but when operating in the program it makes sense to have everything together.
e.g.

struct Person {
   identity: Identity,
   address: String,
   ...
}

struct Identity {
  id: Uuid,
  name: String,
  social_security: String,
}

In the program I would like to keep the Person struct together for ergonomics, but when persisting to disk the Identity.name and Identity.social_security should be stored separately for e.g. security reasons, but both should be indexed by Identity.id.

// person.pub.json
{
  id: "1",
  name: "Steven"
}

// person.secret.json
{
  id: "1",
  social_security: "123-45-6789"
}

To do the persistence to disk I have created a trait that expects an &T where T is the type being serialized.

trait Persist<T> 
where T: Serialize + Deserialize {
  fn data(&self) -> &T;
  fn path(&self) -> PathBuf;
  fn save(&self) -> () {
    serde_json::to_writer(self.path(), self.data()).unwrap();
  }
  ...
}

So the issues arise when attempting to implement Persist<PublicInfo> for Person and Persist<PrivateInfo> for Person. With

struct PublicInfo {
  id: Uuid,
  name: String,
}

struct PrivateInfo {
  id: Uuid,
  social_security: String,
}

The you can just create and return view structs by-value, which themselves contain individual references to each field. &T: Serialize if T: Serialize, so the derive macro should work just fine.

1 Like

Could you elaborate on that further @H2CO3 ?

There's nothing more to it, really.

struct ABC {
    a: String,
    b: PathBuf,
    c: HashSet<String>,
}

struct AB<'a> {
    a: &'a String,
    b: &'a PathBuf,
}

impl ABC {
    pub fn as_ab(&self) -> AB<'_> {
        AB {
            a: &self.a,
            b: &self.b,
        }
    }
}
3 Likes

I've been playing with your suggestion, but am struggling to implement it because of the signature of Persist.data. Would I need to have two structs to handle the lifetime and owned version?

struct ABOwned {
    a: String,
    b: PathBuf,
}

struct ABBorrowed<'a> {
    a: &'a String,
    b: &'a PathBuf,
}

In the Person example, I need an owned version, because sometimes I only load PublicData and don't create a Person with it.

I am also struggling because the Persist trait actually enforces
trait Persist<T> where T: Serialize + DeserializeOwned. Do I need to change DeserialzeOwned to Deserialize?

You can use Cow in this case, to dynamically switch between owned and borrowed data.

4 Likes

Potential variation of this idea (less efficient but less code) that may work for OP—one struct for all views, via Option:

struct ABC {
    a: String,
    b: PathBuf,
    c: HashSet<String>,
}

struct AbcView<'a> {
    a: Option<&'a String>,
    b: Option<&'a PathBuf>,
    c: Option<&'a HashSet<String>>,
}

Or a HashMap from strings (e.g. String or &'static str) to this enum:

enum PersistedValue<'a> {
    String(&'a String),
    PathBuf(&'a PathBuf),
    HashSet(&'a HashSet<String>),
}

OP may be able to use #[serde(untagged)] with the enum.

1 Like

I got it working with the Cows, but something still has a weird code-smell to it, so I'll post back if I find a better solution.

It would look exactly the same as the example snippet I posted, except for changing &'a String to Cow<'a, str>, mutatis mutandis. Playground.

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.