Generics question - is what I'm trying to do not possible?

I have not found a way to do this anywhere I looked.

In the folowing code I'd like Data to be generic on a few numeric types, without making MainEntity generic.

I tried enums and traits, but I run into issues with both.

When I tried to get the Data back from MainEntity, match can't return different types. When it comes to traits I could not get the data back. - I tried both, &dyn and impl trait. I found examples for both, but all examples just implement the Debug or Display trait and print the value in a match. Which is simple, there's no return value and match won't complain.

But I'd like to be able to append Data with various numeric types to MainEntity rows and get it back.

Basically I'd like to limit the scope of generic code to the lower / inner level structs without having to make their container / parent structs generic. Is it possible?

// I'd like to keep this not generic
struct MainEntity {
    rows: Vec<Data>,
}

struct Data {
    name: String,
    // This should be generic
    values: Vec<u8>,
}

pub fn run() {
    let mainentity = MainEntity { rows: vec![
        Data { name: "a".to_string(), values: vec![1_u8, 2_u8, 5_u8] },
        // The next line should work.
        Data { name: "b".to_string(), values: vec![1.0_f32, 2.0_f32, 5.0_f32] },
    ]};
    // The next lines should return Data by the name (via a method):
    let data_a = mainentity.get_data_by_name("a");
    let data_b = mainentity.get_data_by_name("b");
}

Is something like this what you are looking for?

use num::ToPrimitive;

// I'd like to keep this not generic
struct MainEntity {
    rows: Vec<Data>,
}

impl MainEntity {
    pub fn get_data_by_name(&self, name: impl Into<String>) -> &[Data] {
        &self.rows
    }
}

struct Data {
    name: String,
    // This should be generic
    values: Vec<Box<dyn ToPrimitive>>,
}

pub fn run() {
    println!("generic_nested_structs two");
    let mainentity = MainEntity { rows: vec![
        Data { name: "a".to_string(), values: vec![Box::new(1_u8), Box::new(2_u8), Box::new(5_u8)] },
        // The next line should work.
        Data { name: "b".to_string(), values: vec![Box::new(1.0_f32), Box::new(2.0_f32), Box::new(5.0_f32)] },
    ]};
    // The next lines should return Data but the name:
    let data_a = mainentity.get_data_by_name("a");
    let data_b = mainentity.get_data_by_name("b");
}

As you have figured out, what you are looking for is not ergonomic. The only way to make Data values generic and at the same time not making MainEntity generic is using traits.

Maybe if you explain your use case the community would be able to further help you. It's always better to express your intent rather than the method that you think is the right way (which can lead to an XY situation, or unsatisfactory answers).

1 Like

As a study I wrote a program for myself that writes an .exr image file:

Exr image format specification

The code is very naive and now I'd like to learn more about Rust by rewriting it as a library.

An exr image can contain multiple pixel values in channels with each channel having a different pixel value type. And that's what I'm trying to implement. And to be able to make changes to the data I probably need to own the data that I get from the "get_data_by_name" method. Eventually I'd like to be able create / read / update and save exr images with multiple channels with pixel value types of u8, u32, f16, f32, ... And I'd like to minimize the impact of generics.

Also, In the example I can't make MainEntity generic, that would only mean I can create it with multiple data types, but only one at a time.

You can do this with enums and macros. Dependent code would have to deal with the possibility of channels with different pixel types, choose which channel variant to create, etc.

pub trait Channel {
    // (Ignoring error handling)
    fn add_row<R: Read>(&mut self, len: usize, rd: &mut R);
    fn output<W: Write>(&self, wt: &mut W);
    // width, heigh, name, ...
}

#[derive(Clone,Debug,PartialEq)]
pub enum SomeChannel {
    Float(FloatChannel),
    Int(IntChannel),
    // ...
}

impl Channel for SomeChannel {
    // dispatching boilerplate
}

// ...
macro_rules! impl_channel {
    ($name:ident, $read:ident, $write:ident, $inner:ty) => {
        #[derive(Clone,Default,Debug,PartialEq)]
        pub struct $name {
            name: CString,
            width: usize,
            data: Vec<$inner>,
        }

        impl Channel for $name {
            fn add_row<R: Read>(&mut self, len: usize, rd: &mut R) {
                // use $read to read a $inner
            }
            fn output<W: Write>(&self, wt: &mut W) {
                // use $write to write your $inner's
            }
        }
    }
}

impl_channel!(FloatChannel, read_float, write_float, f32);
// ...

How do I get a copy of the data from the SomeChannel enum?

Read / write is great implemented for Channel, but any other operations should be possible with the values in channel, including addig them to another image. I tried something similar, but I always got stuck when match complained about return types not matching.

I suspect you are doing mistake 6c here.

In dynamically-typed languages it's normal to have X that is ā€œA, B, C, or Dā€, Ywhich is ā€œA, C, E, or Fā€ andZ` which is ā€œD, F, M, or Nā€. And then handle few combos you actually care about.

Rust's take on that: of, you have X which is one of four variants, and the same with Y and Zā€¦ great! Just show me how would you handle all 64 cases that may arise here and we'll go from there.

You can add traits which would do that. But of course you would have to answer the question of what should happen to get_data_by_name when floats go in and integers go out. Or you can use Any and it's plethora of downcast methods, but at this point you are half-way to the dynamically typed language both in flexibility and in speed and thus it no longer makes sense to use Rust if that ability is used not for some auxilliary processing but as core part of your design.

The user matches on an enum and handles the data that's actually there, or you have fallible type-specific accessors, or you supply conversion methods between data types.

Or some combination of those.

1 Like