Polymorphism for enum


#1

hi there, i am new to rust and as my first task i am writing a script which digest different types of CSV tables. I got to a point it works nicely but there one issue with my code that is not elegant, here is a snippet which shows the problem.

trait RecordTrait {
    fn identify(&self) -> &str;
}


struct Dog {
    woof: String
}

impl RecordTrait for Dog {
    fn identify(&self) -> &str {
        self.woof.as_str()
    }
}


struct Banana {
    hmmm: String
}

impl RecordTrait for Banana {
    fn identify(&self) -> &str {
        self.hmmm.as_str()
    }
}


enum RecordEnum {
    A(Dog),
    B(Banana),
}

impl RecordTrait for RecordEnum {
    fn identify(&self) -> &str {
        match *self {
            RecordEnum::A(ref x) => x.identify(), // <------ is there a more elegant
            RecordEnum::B(ref x) => x.identify(), // <------   way of doing that ?
        }
    }
}

fn main() {
    let e = RecordEnum::A(Dog{woof: "its_a_dog".to_string()});
    let r: &RecordTrait = &e;

    println!("{:?}", r.identify());
}

the output is:

"its_a_dog"

is there a way to have a better impl RecordTrait for RecordEnum which doesn’t have to match for every enum element as they all implement the same Treat?


#2

Assuming you need the separate structs, trait, and the enum with tuple variants containing the struct then I don’t think there’s any other way. You can write a macro to remove some of the boilerplate, like this half-baked example:

enum RecordEnum {
    A(Dog),
    B(Banana),
    C(Banana),
    D(Banana),
    E(Dog),
}

macro_rules! impl_record {
    ($($variant:tt)* ) => {
        impl $crate::RecordTrait for $crate::RecordEnum {
            fn identify(&self) -> &str {
                match self {
                    $(
                      $crate::RecordEnum::$variant(x) => x.identify(),
                    )*
                }
            }
        }
    };
}

impl_record!(A B C D E);

#3

You could try your luck with generics with something on the lines of:

fn identify<T: RecordTrait>(&self) -> &str {
           self.identify();
}

#4

thanks for the speedy reply :slight_smile: if it helps i need the structs and the enum but i don’t need the trait, i’ve added the trait as i thought it would do the trick here (but it didn’t)


#5

This is the natural choice when you statically know the type you’re working with. Presumably, @vim-zz has the enum because he’s parsing a row from a CSV and it’s not known statically what type it is (but it’s one of the enum variants).


#6

Yeah definitely. But if every variant of the enum satisfies the trait, it may still work right?


#7

If you get rid of the trait and reach into the field, you can pattern match it like so:

struct Dog {
    woof: String,
}

struct Banana {
    hmmm: String,
}

enum RecordEnum {
    A(Dog),
    B(Banana),
}

impl RecordEnum {
    fn identify(&self) -> &str {
        match self {
            RecordEnum::A(Dog { woof: x }) |
            RecordEnum::B(Banana { hmmm: x }) => &x,
        }
    }
}

This requires the x pattern to resolve to the same type across the variants (String in this case).

I don’t know if this is really an improvement, though, as it requires the same field type across the types.


#8

Yes, but I don’t think it makes the match statement any better since you still need to write the same code across the match arms.


#9

Actually, can you explain why you need both the enum and the structs? Do you have some other APIs that deal with just the structs?


#10

well, it is just easier to have a separate struct for each CSV, i guess could have used enum with this types defined inside it. choosing between these two options i thought it be more clear to have a specific struct per CSV spec


#11

If there’s no reason to have the separate structs, then an enum like this is more straightforward and reduces the nesting of the pattern matching:

enum RecordEnum {
    Dog {
        woof: String,
        // more fields
    },
    Banana {
        hmmm: String,
        // more fields
    },
}

impl RecordEnum {
    fn identify(&self) -> &str {
        match self {
            RecordEnum::Dog { woof: x, .. } | RecordEnum::Banana { hmmm: x, .. } => x,
        }
    }
}

Note that this doesn’t allow you to control the visibility of the fields, so if that’s a concern then this is a no go. It sounds like this is for your own internal stuff so likely not an issue.

So to summarize, if you don’t intend to use the nested structs for their own distinct type-ness, then probably skip them.


#12

well, it is a bit more complex as each one of theses types has associated methods which parse each CSV field differently - i hope it explain better why i meant it is clearer to use struct. in the sample here i have omitted this part for clarity.


#13

Ok, that’s clearer indeed. You could factor out parsing into helper types but I don’t think they need to be methods and therefore hold any state themselves (besides maybe some buffer they’re parsing out of). But I’m speculating here …

Have you looked into the https://crates.io/crates/csv crate by the way?


#14

yes i am using that, actually i couldn’t use it in a straight forward way as it doesn’t allow to ‘choose’ the CSV type in runtime, so i first using it to translate the data to JSON and then i use serde_json untagged enum to allow this.

i have to say, as a newbie here, that i have read that the rust community is helpful - and i am happy to experience it myself - thanks a lot @vitalyd and @dylan.dpc


#15

Does a given CSV file contain a single type actually? I initially thought you had the different types intermixed but now need to double check that cause you mentioned something about “different types of CSV tables” in the initial post.


#16

yes, the script goes over CSV files and each can contain different format, but each CSV file contains only a single format.


#17

Ah, so you should be able to use csv’s serde support and select the type to deserialize into. The example in the docs how the type is selected. The entry point code will still need to know statically which type to use, but that can be matched at a higher level. So something like (semi pseudocode):

match file_type {
   "dogs" => read::<Dog>(...),
   "bananas" => read::<Banana>(...),
}

fn read<T: Deserialize + Debug>(...) {
   // using csv's doc example for most of this
   let mut rdr = csv::Reader::from_reader(io::stdin());
   for result in rdr.deserialize() {
        // Notice that we need to provide a type hint for automatic
        // deserialization.
        let record: T = result?; <== type selected here
        println!("{:?}", record);
    }
}

Or did something like that not work?


#18

coming from dynamic languages, my desire was not to rely on any external hints for choosing the right format, so at first i just thought of letting it fail and skip to the next type till it works, but eventually i got to this approach using serde_json untagged enums which really got me to where i wanted - almost perfect for me, beside the topic’s issue of course :slight_smile:


#19

Rust (and the community), in general, will heavily steer you towards doing static type based coding. There’re places where dynamicism is needed, and Rust has some support for it, but you’ll get more out of the language (and likely have an easier time) if you try to use static information. In addition, performance will almost always be better when static types are involved. I realize you probably don’t care too much about performance here since you’re willing to go via json as an intermediate step :slight_smile:.

You can certainly do the “try type X and fail to the next one” even statically. The RecordType enum is already static information about the types of records you expect. So you could do something like:

fn read<T: Deserialize + Debug>(...) -> Result<(), Error> {
    // as before except we return a Result now
}

read::<Dog>(...).or_else(|_| read::<Banana>()).expect("unknown type in the file");

But I think I’ve offered too many different options at this point, so carry on :slight_smile:.


#20

this looks like a good approach, i defiantly give it a try - thanks