Using nested structs with vectors of generic type

The project I'm working on uses nested structs (3 levels) and a vector that takes a generic type. I've received a lot of help here on this forum getting the nested structs to work and, just recently, getting the generic typing on the vectors working. However, when I add data to test the vector with different types of data, I run into mismatched type errors. Which makes sense when you look at how the program is put together. I put some very simplified code in the playground and you can look at it here.

The problem is that the vector field in the first level struct absolutely must be usable with different types, but when I enter data containing different types, a mismatched type error is generated by the compiler. For good reason, I do understand that, but I haven't been able to come up with a workaround. I thought maybe I could use an enum here, somehow, but if that's the case, I sure can't see how to make enums work for what I need. My question to my friends on this forum is whether or not there is a workaround that will let me keep my nested struct approach. Any ideas?

You insert an enum where you need the different types to be handled uniformly. Ie., in your case, around Variable. (Or just make Variable an enum itself.) The other levels from that point on won't need to be generic.

Example.


As an aside, please don't do the // end of impl XYZ thing. It's useless and annoying – we know that closing braces are the end of an item, that's already their meaning.

1 Like

Another option is to use boxed trait objects. They have higher overhead, but allow arbitrary types which meet the trait.

Playground

1 Like

Your use of Vec<Box<dyn Debug>> looks like it will work for my application. Thanks. The type of data being placed in the list is user-determined and thus can't be known at compile time. Your solution looks like it should work. Thanks. I'll get busy incorporating it into my application and let you know how it goes.

I am, admittedly, rather weak in my understanding of how traits work and fit into the grander scheme of the Rust language structure. I imagine that will come with time as I continue to use the language and dig into the learning resources. For now, is it possible that you could enlarge on what dyn Debug is and what it does for me? Thanks.

See the overview of Trait Objects.

Here's a shortened view for this specific case: Box<dyn Debug> is a pointer type which points to heap-allocated memory. A Box<Something> can be coerced to a Box<dyn Debug> as long as the Something:

  • Is fixed size. Examples: String, i32, &str (but not plain str)
  • Implements the Debug trait.

~A trait object itself implements the trait, so Box<dyn Debug> is debug-printable.~ See correction from @quinedot

For Box<dyn Debug>, another requirement is

  • Meets a 'static bound
    • &'static str: 'static so you could box and coerce that to Box<dyn Debug>
    • But &str does not meet the bound for any non-'static lifetime, so you could not perform the coercion in that case

And the reason is that dyn Trait always has a lifetime which is usually elided, and the default lifetime when elided in a Box is 'static. So a Box<dyn Debug> in your struct is really a Box<dyn Debug + 'static>.

(It's possible to have a Box<dyn Debug + 'a> to support non-'static types, but probably you don't want to do that in this case; the lifetime will infect everything. Just duplicate &strs into Strings if you need to, and box those instead.)

Another nit

dyn Trait implements Trait[1] automatically, but Box<dyn Trait> only implements Trait if there's an explicit implementation. This doesn't matter as much as you might think due to deref coercion from Box<T> to &T or &mut T, but it does matter.

In the case of Box<dyn Debug> though, there is an explicit implementation.


  1. almost always ↩︎

3 Likes

I really like and implemented your suggestion of using boxed trait objects. The simplified version, shared above, of my application doesn't show that the user-entered data being stored in those boxes eventually needs to be serialized and saved in JSON format using the serde crate. This is the use statement that access it: use serde::{Deserialize, Serialize};. I simplified the code I shared because I didn't want to make everyone wade through a lot of clutter. Here is the actual code for the Variable struct in its current form:

#[derive(Debug, Serialize, Deserialize)]
struct Variable {
    var_text: String,
    is_num: bool,
    is_signed: bool,
    dcml_places: u32,
    comma_frmttd: bool,
    is_list: bool,
    list_vec: Vec<Box<dyn Debug>>,
}

The problem I am now having is that the compiler doesn't like <dyn Debug> in this context. Here is the error message:

list_vec: Vec<Box<dyn Debug>>,
     |     ^^^^^^^^ the trait `Serialize` is not implemented for `dyn Debug`

So, I've been doing some extra reading on traits, trying to see how all this fits together, but solving this issue is still beyond me. I really like using Box to place the data in the heap, so I'm hoping this solution is still viable, but I need help making it work.

Before I ask you to help me solve this problem could you check on my understanding of the keyword dyn? As I understand it, dyn is short for dynamic and we need it here because the data in list_vec needs to allow for different types, depending on what kind of input is being gathered. Is my thinking correct on that subject?

Thanks for your help getting this working.

Yes

serde creates some issues with trait objects. This should help: https://crates.io/crates/erased-serde/0.3.29

The error has nothing to do with box. The problem is the trait object. Since Serialize::serialize() is generic, it can't be called on a trait object (as it can't be monomorphized and codegenned dynamically). You'll likely need the erased_serde crate.

By the way, using trait objects as your data model is generally a pain. You should probably stick with enums here.

3 Likes

If you do switch to enums, this one is pretty handy instead of rolling your own: Value in serde_json - Rust

Well, there is no question but that it would be a good thing for me to build my skills in working with enums. So, let's explore it. The reason I chose to try out @tbfleming's suggestion using <dyn debug> is that as the application will be used, there is no way to know, at compile time, the type that will fill the vector in the Variable struct. With the test data in my simplified example, it is easy to know the type at compile time, but that isn't how the application will work. These lines from your suggested example indicate to me that we need to know, at compile time, the type that is going to fill the vector. I could easily be wrong, so please correct me.

  let mut testquest1 = Question::new();
    testquest1.qtext = "Is the air outside smoky?".to_string();
    testquest1.var_vec = vec![
        VarWrapper::Str(testvar1),
        VarWrapper::Int(testvar2),
    ];

    let mut testquest2 = Question::new();
    testquest2.qtext = "Is the air outside clear?".to_string();
    testquest2.var_vec = vec![
        VarWrapper::Float(testvar3),
        VarWrapper::Int(testvar4),
    ];

I'm referring to lines 4&5 and 11&12.

How do you fill it, then? Rust is statically typed and has no JIT, so there's no way to come up with new types at runtime.

Is this a library or an application? And do you really need to support an arbitrary set of types? It doesn't look to me like the domain warrants it.

This is an application targeted at teachers that will be used to generate questions for use in worksheets, tests, and quizzes. This particular field will hold user-inputted lists of various types that can then be accessed in random fashion as needed. The type that fills the list is user choice. If the teacher needs to insert random student names into his questions, then the list_vec vector will need to be filled with strings. If he/she is generating practice algebra equations, then the elements of the vector will need to be numeric or perhaps characters (to be used as variables in the equation). I haven't dealt much yet with the user input part of the application. Right now I'm focusing on how to structure and save the data. I will probably use the fltk crate to help with the user interface side of the app.

This is why I asked this question and was my motivation to lean toward using Vec<Box<dyn Debug>> to place the data on the heap. It seems like a good workaround. Getting it to work with serde though, is going to push the learning curve for me. I considered using enums before I posted this topic, but haven't seen a way to get past the static typing you mentioned. (That's not surprising since I'm pretty unskilled at using Rust and at programming in general.) Am I right in saying that trying to use enums to solve my problem is probably a dead end?

Not really; the only thing that enums require is that you have a fixed set of types that you support¹. In exchange for this restriction, you get a lot more flexibility than you do with trait objects (like being able to write special cases for individual variants without the hassle of dealing with the Any trait).

Given what you've written, it sounds like you need to support a vector that contains Strings, chars, and i32s:

enum FieldType {
    Text(String),
    Integer(i32),
    Variable(char),
}

You're going to need some special-case code for each of these types anyway when you write the input routines, and you can pack them up into the appropriate enum variant at that point.


Also, enums allow you to tell apart multiple variants with the same internal type. You could, for example, have both Person(String) and City(String) variants that behave differently despite both containing Strings.


¹ It's also sometimes useful to mix enums with trait objects by having one of the enum's variants hold Box<dyn Something>, or to make an enum generic with a variant that holds T. Both of these options are probably more niche than you're looking for.

But this means that the list of possible types is going to have to be a finite, pre-determined set, because there will be no opportunity whatsoever for the user to generate new Rust types when the application is running.

No. It's exactly what my modified Playground above demonstrated.

1 Like