6

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.

Shepmaster
  • 388,571
  • 95
  • 1,107
  • 1,366
Haffix
  • 128
  • 2
  • 6
  • Given that there's nothing about `Student` as a datatype that implies a specific sort order, I'll second that `sort_by` seems like the way to go. – loganfsmyth Aug 13 '17 at 20:35

2 Answers2

7

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:

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.

Shepmaster
  • 388,571
  • 95
  • 1,107
  • 1,366
Lukas Kalbertodt
  • 79,749
  • 26
  • 255
  • 305
  • Ahh I see. Thanks, this is great. You see, I tried using `sort_by_key(|a| a.last_name)` and I got the error "cannot move out of borrowed content". I don't know why I though this would work (but hey, I'm learning). But this led me to this: Try to figure out how macros work. So harm done, lessons learned :) Again, thank you for a clear and thorough response! – Haffix Aug 13 '17 at 21:14
  • You're welcome! Your `sort_by_key()` invocation has a different problem, which could be solved like so: `sort_by_key(|a| a.last_name.clone())` but it is *very* slow! A better solution would be `sort_by_key(|a| &a.last_name)`, but this doesn't work (we're discussing this problem [in the chat](https://chat.stackoverflow.com/rooms/62927/rust)). Oh and btw, a tip for using StackOverfow: if you like and accept an answer, you should usually upvote it. Otherwise the answerer often falls into despair wondering why he/she didn't deserve the upvote ^_^ – Lukas Kalbertodt Aug 13 '17 at 21:28
  • And no worries I took your lessons to heart. But I do want to point out a possible use case for this. You see, I have another struct `Grade` that's linked to this one: `struct Grade { grade: u8, student_roster: Vec, }`. This one, in my opinion has a natural ordering in a `Vec`. 1st grade < 2nd grade < 7th grade. So, I don't know, I feel this macro has some uses. I mainly feel the code you're writing to get `Ord` implemented can be a bit tedious and not DRY. But hey I'm a noob so maybe I'll have a different opinion later down the road. – Haffix Aug 13 '17 at 21:28
  • I tried! I get an error that I don't have enough repetition :P I need some like 15 (I'm pretty new here, heheh). Would if I could, bro. – Haffix Aug 13 '17 at 21:31
  • @Haffix There are plenty of use cases, absolutely! But many people coming from some other languages tend to implement `Ord` just so they can use `sort` in a specific situation. My answer just wants to make the reader aware of the possibilities with the result that they think about "natural ordering". In the end, the programmer has to decide ;-) – Lukas Kalbertodt Aug 13 '17 at 21:43
2

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