How to repeat without syntax variables in macro?

I am trying to implement PartialOrd that look like this:

#[derive(PartialEq)]
struct Dimensions {
    width: f32,
    height: f32,
}

impl PartialOrd for Dimensions {
    fn partial_cmp(&self, right: &Self) -> Option<std::cmp::Ordering> {
        use std::cmp::Ordering;
        match (self.width.partial_cmp(&right.width)?, self.height.partial_cmp(&right.height)?) {
            (Ordering::Less, Ordering::Less) => Some(Ordering::Less),
            (Ordering::Equal, Ordering::Equal) => Some(Ordering::Equal),
            (Ordering::Greater, Ordering::Greater) => Some(Ordering::Greater),
            _ => None,
        }
    }
}

And I have this macro:

macro_rules! impl_partial_ord {
    [$name:ident {  $($field:ident),* } ] => {
        impl PartialOrd for $name {
            fn partial_cmp(&self, right: &Self) -> Option<std::cmp::Ordering> {
                use std::cmp::Ordering;
                match ( $( self.$field.partial_cmp(&right.$field)? ),* ) {
                    ( $(Ordering::Less),* ) => Some(Ordering::Less),
                    // ...
                    _ => None,
                }
            }
        }
    };
}
impl_partial_ord! {
    Dimensions { width, height }
}

Main problem is that ( $(Ordering::Less),* ) repetition contain no $field,

And rust macro slapped in my face this this error:

error: attempted to repeat an expression containing no syntax variables matched as repeating at this depth
  --> ..\phone.rs:48:28
   |
48 |        ( $(Ordering::Less),* ) => Some(Ordering::Less),
   |           ^^^^^^^^^^^^^^^

How do I achieve similar result ? Playground

Put them in an array and use Iterator::all

Your solution require to call .partial_cmp(..) by 3x, Something like that:

match ( self.width.partial_cmp(&right.width)?, self.height.partial_cmp(&right.height)?, ) {
    (Ordering::Less, Ordering::Less) => return Some(Ordering::Less),
    _ => {},
}
match ( self.width.partial_cmp(&right.width)?, self.height.partial_cmp(&right.height)?, ) {
    (Ordering::Greater, Ordering::Greater) => return Some(Ordering::Greater),
    _ => {},
}
// ...
return None;

I do not want to call self.$field.partial_cmp(&right.$field)? multiple times, for each branch,

And don't want to relay on compiler to optimize that code.

How about this?

macro_rules! impl_partial_ord {
    [$name:ident {  $($field:ident),* } ] => {
        impl PartialOrd for $name {
            fn partial_cmp(&self, right: &Self) -> Option<std::cmp::Ordering> {
                use std::cmp::Ordering;
                $(
                    let Some(Ordering::Less) = self.$field.partial_cmp(&right.$field) else { return None; };
                )*
                Some(Ordering::Less)
            }
        }
    };
}

The output is not correct,

Output should be return
Ordering::Less where every left fields < every right fields.
Ordering::Equal where every left fields == every right fields.
Ordering::Greater where every left fields > every right fields.

No it doesn't. What exactly prevents you from storing it in a variable?

Sorry; I didn't notice the ... comment and just replicated the output of the macro code you posted. This should be better:

macro_rules! impl_partial_ord {
    [$name:ident { } ] => {
        impl PartialOrd for $name {
            fn partial_cmp(&self, right: &Self) -> Option<std::cmp::Ordering> {
                Some(std::cmp::Ordering::Equal)
            }
        }
    };
    
    [$name:ident { $first:ident $(,$field:ident)* } ] => {
        impl PartialOrd for $name {
            fn partial_cmp(&self, right: &Self) -> Option<std::cmp::Ordering> {
                let out = self.$first.partial_cmp(&right.$first)?;
                $(
                    if out != self.$field.partial_cmp(&right.$field)? { return None };
                )*
                Some(out)
            }
        }
    };
}

I believe that @new is looking for a short-circuiting behaviour, where later fields are not compared if the result is determined by the earlier ones.

1 Like

OP's original playground suggests the opposite. It contains the following function:

fn partial_cmp(&self, right: &Self) -> Option<std::cmp::Ordering> {
    use std::cmp::Ordering;
    match ( $( self.$field.partial_cmp(&right.$field)? ),* ) {
        ( $(Ordering::Less),* ) => Some(Ordering::Less),
        _ => None,
    }
}

which would evaluate all comparisons just as eagerly if the disallowed repetition compiled.

This doesn't evaluate any of the per-field comparisons more than once.

The general solution to this problem is to make another branch in the current macro (could also be a separate macro though) that takes $field as input, but doesn't actually use it and instead outputs the tokens you want (Ordering::Less in this case).

Ultimately though the flagged solution should be more efficient.

2 Likes

This topic was automatically closed 90 days after the last reply. We invite you to open a new topic if you have further questions or comments.