Modelling sequence of optional values

Hi,

I've a question on how to model a sequence of optional values, where the n-th value depends on all previous n-1 values.

For context: in a GUI application there is a data type

struct Data {
  subdata_1: Option<i32>,
  subdata_2: Option<i32>,
  subdata_3: Option<i32>,
}

Initially, all subdata_ fields are None. While the user interacts with the program and inputs data, subdata_1 becomes Some(...), after some more interactions, subdata_2 becomes Some(...) (depending on subdata_1.unwrap()), and so on.

Thus, for n < m, when subdata_m is Some(...), then all previous subdata_n must evaluate to Some(...) as well. Consequently, the following is no valid Data instance:

Data {
   subdata_1: None,
   subdata_2: Some(...),
   subdata_3: None,
}

I'd really like to model the invariant above in the data type, such that invalid instances are un-representable. Something like

struct Data (Option<(i32, Option<(i32, Option<i32>)>)>);

which obviously is unreadable and hard to maintain.

Note that the actual types of subdata_ differ, i.e., storing them in a vector is not an Option (pun intended).

Do you have any idea on how to do this?

Sounds like an enum could do.

enum Data {
    InitialState,
    SecondState(i32),
    ThirdState(i32, u64) //etc.
}
3 Likes

Yes, I tried this as well, but I had to write a lot of boilerplate code for accessing the common fields of each variant:

let subdata_1 = match self {
  SomeSelf::InitialState(subdata_1) => subdata_1,
  SomeSelf::SecondState(subdata_1, _) => subdata_1,
  SomeSelf::ThirdState(subdata_1, _, _) => subdata_1,
};

I was hoping there is another way where I do not need to "replicate" each field.

Here's one approach to make it more ergonomic.

2 Likes

Very interesting approach, thanks!

Here's a possible approach where you can use dyn GetData to represent an unknown step. The Any bound lets you then get back the original object when you want to do something specific to a particular step.

It somewhat abuses Deref, so I'm not entirely sure whether I actually think it's a good idea myself.

pub struct StepOne { first: u32 }
pub struct StepTwo { second: u64, step_one: StepOne }

trait GetData: std::any::Any {
    fn try_first(&self)->Option<u32> { None }
    fn try_second(&self)->Option<u64> { None }
    fn try_third(&self)->Option<char> { None }
}

impl StepOne {
    pub fn first(&self)->u32 { self.first }
    pub fn next(self, second: u64) -> StepTwo {
        StepTwo { second, step_one: self }
    }
}

impl GetData for StepOne {
    fn try_first(&self)->Option<u32> { Some(self.first()) }
}

impl StepTwo {
    pub fn second(&self)->u64 { self.second }
}

impl std::ops::Deref for StepTwo {
    type Target = StepOne;
    fn deref(&self)->&StepOne { &self.step_one }
}

impl GetData for StepTwo {
    fn try_first(&self)->Option<u32> { Some(self.first()) }
    fn try_second(&self)->Option<u64> { Some(self.second()) }
}

(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.