AlexKing
AlexKing

Reputation: 131

How to write a generic function or macro to extract specific fields from structs?

I have several asset types modeled as structs, e.g.

#[derive(Debug, Deserialize, Serialize)]
struct Vehicle {
    short: String,
    name: String,
    number_plate: String,
    purchase_cost: u32,
    purchase_date: NaiveDate,
    charge_minute: f32,
    charge_km: f32,
}

The different structs have different fields, but they all have a "short" and "name" fields which implement ToString (in fact they are all Strings)

I'd like to get a Vec of (short, name) pairs, so:

fn vehicle_pairs(vs: &Vec<Vehicle>) -> Vec<(String, String)> {
    vs.iter()
    .map(|v| (v.short.to_string(), v.name.to_string()))
    .collect::<Vec<_>>()
}

Which is fine. To generalize this function to other types, I probably need a macro since to make it generic I'd need to specify bounds that relate to fields rather than methods and I don't believe this is possible.

I'm new to macros, just read some docs today. My first attempt:

macro_rules! pairs {
    ($T:ty) => {
        fn get_pairs<T>(xs: &Vec<T>) -> Vec<(String, String)> {
            xs.iter()
                .map(|x| (x.short.to_string(), x.name.to_string()))
                .collect::<Vec<_>>()
        }
    }
}

pairs!(Vehicle);

But this doesn't work.

How can I create a generic function that extracts (short, name) from a struct with those fields?

Upvotes: 0

Views: 87

Answers (2)

FZs
FZs

Reputation: 18619

A generic function means that one single implementation has to work for all types you can pass in for T (ones that match the bounds).

We can't do that, because our macro only implements this function for certain types. The first thing we can try is instead of being generic at all, just make the macro create a specific function for each type we call it with:

macro_rules! pairs {
    ($T:ty) => {
        // Make the function an associated function, so we don't have name collisions if we use the macro on multiple structs.
        impl $T {
            //                    vv--- Using the specific type received as argument
            fn get_pairs(xs: &Vec<$T>) -> Vec<(String, String)> {
                xs.iter()
                    .map(|x| (x.short.to_string(), x.name.to_string()))
                    .collect::<Vec<_>>()
            }
        }
    }
}

pairs!(Vehicle);

Playground

That works.

But we can actually make the function generic if we use a trait. In the trait, you specify the requirements to the struct (we need a method that, given a struct, returns a (short, name) pair). Then, we use a macro to implement it for all the types we need. Finally, we can make a generic function, which works on an array of those.

So, here it is:

trait ToShortAndName {
    fn to_short_and_name(&self) -> (String, String);
}

macro_rules! impl_to_short_and_name {
    ($T:ty) => {
        impl $crate::ToShortAndName for $T {
            fn to_short_and_name(&self) -> (String, String) {
                (self.short.to_string(), self.name.to_string())
            }
        }
    }
}

impl_to_short_and_name!(Vehicle);

fn get_pairs<T: ToShortAndName>(xs: &Vec<T>) -> Vec<(String, String)> {
    xs.iter()
    .map(|x| x.to_short_and_name())
    .collect::<Vec<_>>()
}

Playground

Great!

We can make this more general by making our function take &[T] as an argument instead of &Vec<T> (it never makes sense to take &Vec<Anything> as argument).

Actually, in this case we can be even more general by allowing any impl IntoIterator<Item = &T>, which &Vec<T> satisfies:

trait ToShortAndName {
    fn to_short_and_name(&self) -> (String, String);
}

macro_rules! impl_to_short_and_name {
    ($T:ty) => {
        impl $crate::ToShortAndName for $T {
            fn to_short_and_name(&self) -> (String, String) {
                (self.short.to_string(), self.name.to_string())
            }
        }
    }
}

impl_to_short_and_name!(Vehicle);

fn get_pairs<'a, T: ToShortAndName + 'a>(xs: impl IntoIterator<Item = &'a T>) -> Vec<(String, String)> {
    xs
    .into_iter()
    .map(|x| x.to_short_and_name())
    .collect::<Vec<_>>()
}

Playground

Upvotes: 3

prog-fh
prog-fh

Reputation: 16785

I think there is a confusion between the generic aspect of a macro and the generic parameter <T> in our attempt.

I understand that you consider that get_pairs() applies to a sequence of something having two members short and names convertible into String.

First, I would, introduce a trait with a get_pair() function providing the two expected strings. Then a macro can help its implementation for any type we find relevant.

The get_pairs() function simply relies on the previous trait when considering the elements of a sequence.

And if we want something a bit more generic, we can rely on iterators instead of slices as a sequence.

#[derive(Debug)]
struct Vehicle {
    short: String,
    name: String,
    number_plate: String,
    purchase_cost: u32,
    charge_minute: f32,
    charge_km: f32,
}

#[derive(Debug)]
struct Pet {
    short: String,
    name: String,
    age: u32,
}

trait GetPair {
    fn get_pair(&self) -> (String, String);
}

macro_rules! impl_getpair_on_short_and_name {
    ($T:ty) => {
        impl GetPair for $T {
            fn get_pair(&self) -> (String, String) {
                (self.short.to_string(), self.name.to_string())
            }
        }
    };
}
impl_getpair_on_short_and_name!(Vehicle);
impl_getpair_on_short_and_name!(Pet);

fn get_pairs<T: GetPair>(seq: &[T]) -> Vec<(String, String)> {
    seq.iter().map(|x| x.get_pair()).collect()
}

// when adding support for iterators,
//   the previous   get_pairs()   becomes useless

fn get_pairs_it<'a, T, S>(seq: S) -> Vec<(String, String)>
where
    T: GetPair + 'a,
    S: IntoIterator<Item = &'a T>,
{
    seq.into_iter().map(|x| x.get_pair()).collect()
}

fn main() {
    let vehicles =
        Vec::from_iter(["A", "B", "C"].into_iter().map(|n| Vehicle {
            short: format!("short_{}", n),
            name: format!("name_{}", n),
            number_plate: format!("plate_{}", n),
            purchase_cost: 0,
            charge_minute: 0.0,
            charge_km: 0.0,
        }));
    println!("vehicles: {:#?}", vehicles);
    let pairs = get_pairs(&vehicles);
    println!("pairs from slice of vehicles:\n  {:?}", pairs);
    let pairs = get_pairs_it(vehicles.iter());
    println!("pairs from iterator on vehicles:\n  {:?}", pairs);
    //
    let pets = Vec::from_iter(["dog", "cat"].into_iter().map(|n| Pet {
        short: format!("short_{}", n),
        name: format!("name_{}", n),
        age: 0,
    }));
    println!("pets: {:#?}", pets);
    let pairs = get_pairs(&pets);
    println!("pairs from slice of pets:\n  {:?}", pairs);
    let pairs = get_pairs_it(&pets); // .iter() can even be omitted
    println!("pairs from iterator on pets:\n  {:?}", pairs);
}
/*
vehicles: [
    Vehicle {
        short: "short_A",
        name: "name_A",
        number_plate: "plate_A",
        purchase_cost: 0,
        charge_minute: 0.0,
        charge_km: 0.0,
    },
    Vehicle {
        short: "short_B",
        name: "name_B",
        number_plate: "plate_B",
        purchase_cost: 0,
        charge_minute: 0.0,
        charge_km: 0.0,
    },
    Vehicle {
        short: "short_C",
        name: "name_C",
        number_plate: "plate_C",
        purchase_cost: 0,
        charge_minute: 0.0,
        charge_km: 0.0,
    },
]
pairs from slice of vehicles:
  [("short_A", "name_A"), ("short_B", "name_B"), ("short_C", "name_C")]
pairs from iterator on vehicles:
  [("short_A", "name_A"), ("short_B", "name_B"), ("short_C", "name_C")]
pets: [
    Pet {
        short: "short_dog",
        name: "name_dog",
        age: 0,
    },
    Pet {
        short: "short_cat",
        name: "name_cat",
        age: 0,
    },
]
pairs from slice of pets:
  [("short_dog", "name_dog"), ("short_cat", "name_cat")]
pairs from iterator on pets:
  [("short_dog", "name_dog"), ("short_cat", "name_cat")]
*/

Upvotes: 3

Related Questions