-2

I have data that is versioned using SemVer. I have functions that have SemVer requirements, e.g. they support versions in the half-open interval [1.1, 2.0). In C++ I could do something like this:

struct Data_v1_0 {
  // Added in V1.0
  int a;
};

struct Data_v1_1 : public Data_v1_0 {
  // Added in V1.1
  int b;
}

void foo(const Data_v1_0& data) {
  // Supports [1.0, 2.0)
  print(data.a);
}

void bar(const Data_v1_1& data) {
  // Supports [1.1, 2.0)
  print(data.b);
}

Is there any way to do something similar in Rust?

trent
  • 25,033
  • 7
  • 51
  • 90
Timmmm
  • 88,195
  • 71
  • 364
  • 509
  • If your struct has private fields, it can only be constructed from within your crate, so adding new fields is a backwards-compatible change anyway. – Sven Marnach Apr 14 '20 at 09:50
  • Why would you make a new class in C++ actually? – mcarton Apr 14 '20 at 09:53
  • 5
    Try to avoid these kinds of invasive structural constraints. Rather, make your logic operate on the latest version of the data structure, and apply data migrations at the entry and exit points. As you add more versions, users of the oldest data structures will experience the most performance impact, while users of the newest format will experience zero impact. Your code will be clean because it only needs to support the latest structures. – Peter Hall Apr 14 '20 at 11:10
  • 1
    This seems like an unusual context to ask for inheritance, but that aside, the question is very similar to other questions such as [Is it possible for one struct to extend an existing struct, keeping all the fields?](https://stackoverflow.com/questions/32552593/is-it-possible-for-one-struct-to-extend-an-existing-struct-keeping-all-the-fiel) If you have a question that isn't answered there, please [edit] your question to make it more specific. – trent Apr 14 '20 at 13:56
  • 1
    Other questions about inheritance and Rust: [How to avoid code duplication of different structs with semantically equal fields/properties?](https://stackoverflow.com/q/39429218/3650362) and [What is the best way to inherit a struct in Rust 1.3?](https://stackoverflow.com/q/32736170/3650362) – trent Apr 14 '20 at 13:59
  • Could you add a rationale why you would want data structures versioned like this? Looks insane to me, to be honest – kreo Apr 14 '20 at 14:08
  • 1
    Embedding one struct inside another, as your C++ code does, only seems to allow for *adding* new fields. Adding new fields is a semver-compatible change, so it doesn't seem like your proposed (current?) C++ solution best fits your own requirements. – Shepmaster Apr 14 '20 at 14:17
  • @Shepmaster: Yes exactly. So if I load a `Data_V1_1` I can pass it to `foo()` and `bar()` but if I load a `Data_V1_0` I can only pass it to `foo`. That's how SemVer is supposed to work. I don't get your point to be honest. – Timmmm Apr 14 '20 at 15:45
  • So the desired solution never has to care about V2? – Shepmaster Apr 14 '20 at 15:56

2 Answers2

2

You can use traits for this:

trait DataV1_0 {
    fn a(&self) -> i32;
}

trait DataV1_1: DataV1_0 {
    fn b(&self) -> i32;
}

struct Data {
    a: i32,
    b: i32,
}

impl DataV1_0 for Data {
    fn a(&self) -> i32 {
        self.a
    }
}

impl DataV1_1 for Data {
    fn b(&self) -> i32 {
        self.b
    }
}

fn foo(data: &dyn DataV1_0) {
    println!("a: {}", data.a());
}

fn bar(data: &dyn DataV1_1) {
    println!("b: {}", data.b());
}

We use trait inheritance to replace the struct here and trait methods to access the fields. My example is read-only, but you can implement setter methods as well.

Richard Matheson
  • 1,125
  • 10
  • 24
  • Hmm yes ok. Although with your solution there's technically no way to actually *make* a V1.0 `Data`. You can set `b` to a default value I guess, but then nothing prevents you accidentally passing your V1.0 `Data` to `bar()`. – Timmmm Apr 14 '20 at 10:25
  • 1
    So wouldn't you have to do `struct Data_v1_0s { a: i32, } struct Data_v1_1s { a: i32, b: i32, }` and then `impl Data_v1_0 for Data_v1_0s {}` and `impl Data_v1_1 for Data_v1_0s {}` and `impl Data_v1_1 for Data_v1_1s {}`. That is going to get very tedious! Probably requires macros. – Timmmm Apr 14 '20 at 10:27
  • @Timmm you could easily implement a different struct that only implements `Data_v1_0`. That cannot be passed into `bar`. – Richard Matheson Apr 14 '20 at 11:36
  • 1
    I'd echo @peter-hall's comment on the question as well, that it's better off not having versioned data structures, but use data migrations. If you've ever worked with COM and versioned interfaces you'd appreciate the pain of maintaining this stuff. – Richard Matheson Apr 14 '20 at 11:38
1

Another solution is to implement inheritance like it would be done in C. I'm not saying that you should do this, but that it's one possibility to consider:

pub struct Data_v1_0 {
    // Added in V1.0
    a: i32,
}

impl Data_v1_0 {
    fn a(&self) -> i32 {
        self.a
    }
    fn set_a(&mut self, a: i32) {
        self.a = a
    }
}

pub struct Data_v1_1 {
    extends: Data_v1_0,
    // Added in V1.1
    b: i32,
}

impl Data_v1_1 {
    fn b(&self) -> i32 {
        self.b
    }
    fn set_b(&mut self, b: i32) {
        self.b = b
    }
}

impl Deref for Data_v1_1 {
    type Target = Data_v1_0;

    fn deref(&self) -> &Self::Target {
        &self.extends
    }
}

impl DerefMut for Data_v1_1 {
    fn deref_mut(&mut self) -> &mut Self::Target {
        &mut self.extends
    }
}

Thanks to Deref and DerefMut, you can call a() and set_a() on instances of Data_v1_1. However, calling it with the fully qualified syntax (e.g. Data_v1_1::a(data)) is not possible, so replacing Data_v1_0 with Data_v1_1 would be an API breaking change.

Shepmaster
  • 388,571
  • 95
  • 1,107
  • 1,366
Aloso
  • 5,123
  • 4
  • 24
  • 41