Struct with Generic field value which is None initially

Hello!

I seem to be struggling with object safety.

My code cannot compile if any of the following:

  1. If i create a generic struct field with None as value.
  2. If i create a new() method, which will be used to populate with None as value.
    Both situations give different errors. I have commented out situation 2 in the code.

Background: At the creation of the metric structure, the program is awaiting its first value

use std::any::Any;

trait Default {
    fn set_value(&mut self, new_value: Box<dyn Any + 'static>);
    // fn new(name: String) -> Metric<T>;
}
#[derive(Debug)]
struct Metric<T> {
    name: String,
    value: Option<T>,
}

impl<T: Clone + 'static> Default for Metric<T> {
    fn set_value(&mut self, new_value: Box<dyn Any + 'static>) {
        match new_value.downcast_ref::<Option<T>>() {
            Some(val) => self.value = val.clone(),
            None => panic!("Type mismatch"),
        }
    }
    /* fn new(name: String) -> Metric<T> {
        Metric { name, value: None }
    } */
}

fn main() {
    let metric_i32 = Metric {
        name: String::from("my/first/metric"),
        value: Some(42),
    };
    let metric_string = Metric {
        name: String::from("my/second/metric"),
        value: None,
    };
    let mut vec_of_unknown_metrics: Vec<Box<dyn Default>> =
        vec![Box::new(metric_i32), Box::new(metric_string)];

    vec_of_unknown_metrics[0].set_value(Box::new(Some(1234)));
    vec_of_unknown_metrics[1].set_value(Box::new(Some(String::from("fdsf"))));

    /*  let mut vec_of_unknown_metrics2: Vec<Box<dyn Default>> = vec![
        Box::new(Metric::new(String::from("my/first/metric/path/name"))),
        Box::new(Metric::new(String::from("my/second/metric/path/name")))
    ]; */
}

Does anyone have any ideas of how to fix this (if even possible) ?

None is not a type by itself, it's an enum variant (from the Option enum).

In order to make your code work, you need to hint the compiler the generic type:

value: None::<i32>
2 Likes

Is this the only way?

I actually know what values are going to come, and this is stored in an enum DataType. Is it possible to "hint" None:: based on DataType.

In other words how to use a string to set None::? "Pseudo:{value: String::from("None::<i32>"} "

pub enum DataType {
    Int8,
    Int16,
    Int32,
}
impl DataType {
    fn get_rust_type(&self) -> String {
        match self {
            Self::Int8 => String::from("i8"),
            Self::Int16 => String::from("i16"),
            Self::Int32 => String::from("i32"),
        }
    }
}

struct Metric<T> {
    name: String,
    value: Option<T>,
    datatype: DataType,
}

I would then model things differently and not use generics at all. Generic parameters are used when you want the caller (in this case, when the instance of Metric is created`) to define it.

You could model your data using the enum as a sum type like this instead:

enum Metric {
  Int8(MetricInt8),
  Int16(MetricInt16),
  Int32(MetricInt32),
}

struct MetricInt8 {
    name: String,
    value: Option<i8>,
    datatype: DataTypeInt8,
}

struct MetricInt16 {
    name: String,
    value: Option<i16>,
    datatype: DataTypeInt16,
}

struct MetricInt32 {
    name: String,
    value: Option<i32>,
    datatype: DataTypeInt32,
}

struct DataTypeInt8;
struct DataTypeInt16;
struct DataTypeInt32;

This is just a hint on how to do it. The real code would vary depending on your real needs (i.e. if you are using Serde for serialization and deserialization).

1 Like

You could create a Metric based on each variant and return a Box<dyn Default> (p.s. rename this trait). But this has a very "concrete abstraction" feel. Use an enum for the data itself, perhaps.

Why generate and parse a string if you already have the DataType? And how is this an improvement over a compile time turbofish, for that matter?

1 Like

Thank you for the tip! I have been thinking about the same approach. But this makes everything else messy.

I have 500k of Metrics, and iterating on them in one place would be easy. But now they would all be stored in different struct, which i would have to access separately, if I understand your solution.

Some usecases:

for metric in metrics{
   send( serde_json::to_string(&metrics).unwrap() )
}

and

for value in value_update_array{
   metric[value.index].update(value.val);
}

Sorry for the bad pseudo code

Do you mean something to the likes of what @moy2010 mentioned?

Hum, not really. You still have a single type grouping all metrics (the Metric enum). I only changed the representation from being an open one (with generics) to a closed one (with an enum).

If you have logic that is pertinent to the Metric model, you can also add methods to it:

enum Metric {
  ... 
}

impl Metric {
  pub fn do_something_with_metric(&self) {
    match self {
      // ...
    }
  }
}

In that way, you still handle all the metric-related logic in one place.

If this doesn't answer your question, it would be better if you provide a more realistic scenario of your problem and the painpoints that you are experiencing when developing the API.

Yes.

Thx guys! I created a new thread for the actual realistic scenario.