Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

Is it possible to create a macro that implements Ord by delegating to a struct member?

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.

like image 746
Haffix Avatar asked Mar 08 '23 08:03

Haffix


2 Answers

Yes, you can, but first: please read why you shouldn't!


Why not?

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.

How to do it then?

There are two other sort methods:

  • sort_by(), and
  • sort_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.

But seriously, I want a macro...

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:

  1. Add the macro definition around your code and think about what parameters we need (type_name and field)
  2. Replace all your mentions of 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.

like image 126
Lukas Kalbertodt Avatar answered Mar 10 '23 21:03

Lukas Kalbertodt


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);
like image 28
Kris Pinespear Avatar answered Mar 10 '23 21:03

Kris Pinespear