I have a struct:
struct Student {
first_name: String,
last_name: String,
}
I want to create a Vec<Student>
that can be sorted by last_name
. I need to implement Ord
, PartialOrd
and PartialEq
:
use std::cmp::Ordering;
impl Ord for Student {
fn cmp(&self, other: &Student) -> Ordering {
self.last_name.cmp(&other.last_name)
}
}
impl PartialOrd for Student {
fn partial_cmp(&self, other: &Student) -> Option<Ordering> {
Some(self.cmp(other))
}
}
impl PartialEq for Student {
fn eq(&self, other: &Student) -> bool {
self.last_name == other.last_name
}
}
This can be quite monotonous and repetitive if you have a lot of structs with an obvious field to sort by. Is it possible to create a macro to automatically implement this?
Something like:
impl_ord!(Student, Student.last_name)
I found Automatically implement traits of enclosed type for Rust newtypes (tuple structs with one field), but it's not quite what I'm looking for.
Yes, you can, but first: please read why you shouldn't!
When a type implements Ord
or PartialOrd
it means that this type has a natural ordering, which in turn means that the ordering implemented is the only logical one. Take integers: 3 is naturally smaller than 4. There are other useful orderings, for sure. You could sort integers in decreasing order instead by using a reversed ordering, but there is only one natural one.
Now you have a type consisting of two strings. Is there a natural ordering? I claim: no! There are a lot of useful orderings, but is ordering by the last name more natural than ordering by the first name? I don't think so.
There are two other sort methods:
sort_by()
, andsort_by_key()
.Both let you modify the way the sorting algorithm compares value. Sorting by the last name can be done like this (full code):
students.sort_by(|a, b| a.last_name.cmp(&b.last_name));
This way, you can specify how to sort on each method call. Sometimes you might want to sort by last name and other times you want to sort by first name. Since there is no obvious and natural way to sort, you shouldn't "attach" any specific way of sorting to the type itself.
Of course, it is possible in Rust to write such a macro. It's actually quite easy once you understand the macro system. But let's not do it for your Student
example, because -- as I hope you understand by now -- it's a bad idea.
When is it a good idea? When only one field semantically is part of the type. Take this data structure:
struct Foo {
actual_data: String,
_internal_cache: String,
}
Here, the _internal_cache
does not semantically belong to your type. It's just an implementation detail and thus should be ignored for Eq
and Ord
. The simple macro is:
macro_rules! impl_ord {
($type_name:ident, $field:ident) => {
impl Ord for $type_name {
fn cmp(&self, other: &$type_name) -> Ordering {
self.$field.cmp(&other.$field)
}
}
impl PartialOrd for $type_name {
fn partial_cmp(&self, other: &$type_name) -> Option<Ordering> {
Some(self.cmp(other))
}
}
impl PartialEq for $type_name {
fn eq(&self, other: &$type_name) -> bool {
self.$field == other.$field
}
}
impl Eq for $type_name {}
}
}
Why do I call such a big chunk of code simple you ask? Well, the vast majority of this code is just exactly what you have already written: the impls
. I performed two simple steps:
type_name
and field
)Student
with $type_name
and all your mentions of last_name
with $field
That's why it's called "macro by example": you basically just write your normal code as an example, but can make parts of it variable per parameter.
You can test the whole thing here.
I created a macro which allows implementing Ord
by defining expression which will be used to compare elements: ord_by_key::ord_eq_by_key_selector, similar to what you were asking.
use ord_by_key::ord_eq_by_key_selector;
#[ord_eq_by_key_selector(|s| &s.last_name)]
struct Student {
first_name: String,
last_name: String,
}
If you have to sort by different criteria in different cases, you can introduce a containers for your struct which would implement different sorting strategies:
use ord_by_key::ord_eq_by_key_selector;
struct Student {
first_name: String,
last_name: String,
}
#[ord_eq_by_key_selector(|(s)| &s.first_name)]
struct StudentByFirstName(Student);
#[ord_eq_by_key_selector(|(s)| &s.last_name, &s.first_name)]
struct StudentByLastNameAndFirstName(Student);
If you love us? You can donate to us via Paypal or buy me a coffee so we can maintain and grow! Thank you!
Donate Us With