Как написать общую функцию или макрос для извлечения определенных полей из структур?

У меня есть несколько типов активов, смоделированных как структуры, например.

#[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,
}

Различные структуры имеют разные поля, но все они имеют поля «короткое» и «имя», которые реализуют ToString (на самом деле все они являются строками).

Я хотел бы получить Vec пар (короткое имя), поэтому:

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

Это нормально. Чтобы обобщить эту функцию на другие типы, мне, вероятно, понадобится макрос, поскольку, чтобы сделать ее универсальной, мне нужно будет указать границы, относящиеся к полям, а не к методам, и я не верю, что это возможно.

Я новичок в макросах, только что прочитал сегодня несколько документов. Моя первая попытка:

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);

Но это не работает.

Как я могу создать общую функцию, которая извлекает (коротко, имя) из структуры с этими полями?

Почему Python в конце концов умрет
Почему Python в конце концов умрет
Последние 20 лет были действительно хорошими для Python. Он прошел путь от "просто языка сценариев" до основного языка, используемого для написания...
0
0
80
2
Перейти к ответу Данный вопрос помечен как решенный

Ответы 2

Я думаю, что в нашей попытке возникла путаница между общим аспектом макроса и универсальным параметром <T>.

Я понимаю, что вы считаете, что get_pairs() применяется к последовательности, состоящей из двух членов short и names, конвертируемых в String.

Во-первых, я бы представил признак с функцией get_pair(), предоставляющей две ожидаемые строки. Тогда макрос может помочь в его реализации для любого типа, который мы считаем подходящим.

Функция get_pairs() просто полагается на предыдущий признак при рассмотрении элементов последовательности.

А если нам нужно что-то более общее, мы можем полагаться на итераторы вместо срезов в качестве последовательности.

#[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")]
*/

Отличный ответ, спасибо. Я думал о текстовых макросах, где я могу создать функцию get_pairs_vehicle и еще одну функцию get_pairs_pet из макроса. Но не было возможности объединить имя типа в фн. Чего мне не хватало, так это impl $T.

AlexKing 06.07.2024 23:38

«Но не было возможности объединить имя типа с fn» есть concat_idents , который, к сожалению, недостаточно мощный, но есть также Paste::paste!(), который подхватывает места, где отваливается стандартная библиотека.

cafce25 08.07.2024 15:53
Ответ принят как подходящий

Универсальная функция означает, что одна реализация должна работать для всех типов, которые вы можете передать T (тех, которые соответствуют границам).

Мы не можем этого сделать, поскольку наш макрос реализует эту функцию только для определенных типов. Первое, что мы можем попробовать, — это вместо того, чтобы вообще быть универсальным, просто заставить макрос создавать определенную функцию для каждого типа, с помощью которого мы его вызываем:

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);

Детская площадка

Это работает.

Но на самом деле мы можем сделать функцию универсальной, если воспользуемся признаком. В трейте вы указываете требования к структуре (нам нужен метод, который по заданной структуре возвращает пару (короткое, имя)). Затем мы используем макрос, чтобы реализовать его для всех нужных нам типов. Наконец, мы можем создать универсальную функцию, которая работает с их массивом.

Итак, вот оно:

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<_>>()
}

Детская площадка

Большой!

Мы можем сделать это более общим, заставив нашу функцию принимать &[T] в качестве аргумента вместо &Vec<T> (никогда не имеет смысла принимать &Vec<Anything> в качестве аргумента).

На самом деле, в этом случае мы можем быть еще более общими, разрешив любой impl IntoIterator<Item = &T>, который &Vec<T> удовлетворяет:

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<_>>()
}

Детская площадка

Другие вопросы по теме