Problems representing a structure in the type system

My current idea is this:

struct Foo { // A Foo
    bars: Vec<Bar>, // The Foo's Bars
    type_: FooType, // What kind of Foo it is
}

struct FooType { // A kind of Foo
    mandatory_bars: Vec<BarType>,
    optional_bars: Vec<BarType>,
}

struct BarType { // A kind of Bar
    name: String, // The name of the kind of Bar
    type_: BarValueType, // What type the value of the Bar should have
}

enum BarValueType { // The type of the value of a Bar
    Int,
    Float,
    String,
    // etc.
}

struct Bar { // A Bar
    type_: BarType, // What kind of Bar it is
    value: todo!(), // The value of the Bar; but how do I represent the type?
}

The problem with this is that I can't represent the value of a Bar. AFAIK there is no Any type that I can use to dynamically check if the types match, and I don't think I can somehow make it only accept the type that the BarType wants. How can I refactor this so it works with the type system?

I just found out that std::any::Any might actually be a solution, but I'm still figuring out how exactly, as I still want to verify that the type is correct.

Looks like XY problem to me. Obvious mistake #6c.

What language are you trying to write in Rust and what's the actual problem you are trying to solve?

1 Like

you are reinventing sum types. just use an enum with fields.

enum Bar {
    Int(i32),
    Float(f32),
    String(String),
    // etc
}
1 Like

The BarType contains more information than just what type the value is; this doesn't contain all the information I need.

I did my best not to make it an XY problem and give as much of the information as possible, and this is really close to what I actually want. I'm trying to make something like Wikdata; I have Items (Foo), which have Propertys (Bar), which have a specific PropertyType (BarType), which enforces a specific type for the value of the Property. The exact structure of this doesn't matter, but that's the information I want it to contain.

I've found that using std::any::Any kind of does what I want (I think), but I don't think it's a good practice or (possibly) the best solution.
I can verify during construction that the Bar.value has the correct type by using something like:

impl BarValueType {
    fn verify(&self, value: Box<dyn Any>) -> bool {
        match self {
            PropertyType::Int => value.is::<i32>(),
            PropertyType::Float => value.is::<f32>(),
            PropertyType::String => value.is::<String>(),
        }
    }
}

currently, enum with common fields (different terms might be used, like "virtual struct") are not directly supported. you must manually layout the data types.

method 1:

enum PropertyValue {
    Int(i32),
    Float(f32),
    String(String),
}
struct Property {
    name: String,
    value: PorpertyValue,
}
// usage example 1:
println!("property name: {}", prop.name);
match prop.value {
    PropertyValue::Int(ivalue) => println!("property is integer, value: {}", ivalue),
    PropertyValue::Float(fvalue) => todo!(),
    PropertyValue::String(svalue) => todo!(),
}
// usage example 2:
match prop {
    Property { name, value: PropertyValue::Int(ivalue) } => {
        println!("property is integer, name is {}, value is {}", name, ivalue);
    }
    Property { name, value: PropertyValue::Float(fvalue) } => todo!(),
    Property { name, value: PropertyValue::String(svalue) } => todo!(),
}

method 2:

enum Property {
    Int {
        name: String,
        value: i32,
    },
    Float {
        name: String,
        value: f32,
    },
    String {
        name: String,
        value: String,
    }
}
// usage example
match prop {
    Property::Int {  name, value: ivalue } => {
        println!("property is integer, name is {}, value is {}", name, ivalue);
    }
    Property::Float {  name, value: fvalue } => todo!(),
    Property::String {  name, value: svalue } => todo!(),
}
1 Like

Thanks, these make sense! I have one question left: these seem to have no benefit over using dyn std::any::Any. Is there any problem with using that? Does that cause any problems?

Well, it appears there is:

error[E0277]: the size for values of type `(dyn std::any::Any + 'static)` cannot be known at compilation time
   --> src/lib.rs:6:17
    |
6   |     properties: Vec<PropertyValue>,
    |                 ^^^^^^^^^^^^^^^^^^ doesn't have a size known at compile-time
    |
    = help: within `PropertyValue`, the trait `std::marker::Sized` is not implemented for `(dyn std::any::Any + 'static)`
note: required because it appears within the type `PropertyValue`
   --> src/lib.rs:70:8
    |
70  | struct PropertyValue {
    |        ^^^^^^^^^^^^^
note: required by a bound in `std::vec::Vec`
    |
396 | pub struct Vec<T, #[unstable(feature = "allocator_api", issue = "32838")] A: Allocator = Global> {
    |                ^ required by this bound in `Vec`

For more information about this error, try `rustc --explain E0277`.

I'll stick to using method 1 for now, I think. Thanks for your answers!

the most significant difference is enums don't need heap allocation, trait objects must be boxed because they are dynamically sized types.

another difference is, enums are closed, and compiler can check the exhaustiveness statically; trait objects are open, at runtime it can have different concrete types than you might expect, so you'll always need an catch all condition, typically in the form of "failure", "other" or "unknown", and for the known cases, if you accidentally miss a check, compiler won't tell you. for example:

enum PropertyValue {
    Int(i32),
    Float(f32),
}
// the following check didn't cover all variants, and compiler can check
match prop {
    PropertyValue::Int(value) => println!("integer value: {}", value),
    // error: missing `PorpertyValue::Float`
}

// using trait object
enum PropertyType {
    Int,
    Float,
}
struct PropertyAny {
    r#type: PropertyType,
    value: Box<dyn Any>,
}
// you can give it wrong type such as `String`, compiler won't catch the error:
let prop = PropertyAny {
    r#type: PropertyType::Int,
    value: Box::new(String::from("hello")),
};
// if you check `prop.r#type`, the value might mismatch and panic
match prop.r#type {
    PropertyType::int => {
        // this can mismatch and panic
        let ivalue= prop.value.down_cast::<i32>().unwrap();
    }
    PropertyType::Float => {
        // this can mismatch and panic
        let fvalue = prop.value.down_cast:<f32>().unwrap();
    }
}
// if you rely on the dynamic type, this can be non-exhaustive, and you also need to handle the "unknown" cases
if let Ok(i32_ref) = prop.value.down_cast_ref::<i32>() {
    // missing the f32 case, compiler won't check
} else {
    // handle unknown case
    println!("unknown type");
}
3 Likes

Thank you!