How should I implement `PartialOrd` for a enum type manually?

I want to implement PartialOrd manually for a enum type with several variants, for example:

enum Foo<T> {
    Struct0 {},
    Struct1 { foo: u32 },
    Struct2 { foo: u32, bar: PhantomData<T> },
    Tuple0(),
    Tuple1(u32),
    Tuple2(u32, PhantomData<T>),
    Unit,
}

I don’t want to use #[derive(PartialOrd)] because it requires T being PartialOrd, which I don’t want. I imagine an implementation like this should work:

impl<T> PartialOrd for Foo<T> {
    fn partial_cmp(&self, other: &Foo<T>) -> Option<Ordering> {
        match (self, other) {
            (Self::Struct0 {}, Self::Struct0 {}) => ...,
            (Self::Struct1 { .. }, Self::Struct1 { .. }) => ...,
            (Self::Struct2 { .. }, Self::Struct2 { .. }) => ...,
            (Self::Tuple0(), Self::Tuple0()) => ...,
            (Self::Tuple1(..), Self::Tuple1(..)) => ...,
            (Self::Tuple2(..), Self::Tuple2(..)) => ...,
            (Self::Unit, Self::Unit) => ...,
            _ => std::mem::discriminant(&self).partial_cmp(&std::mem::discriminant(&other)),
        }
    }
}

But It does not, because Discriminant does not implement PartialOrd.

I tried to expand the PartialOrd macro, and saw it uses ::core::intrinsics::discriminant_value function, which is not stabilized, so I can’t copy its behavior. I also don’t want to write 49 match arms like this:

impl<T> PartialOrd for Foo<T> {
    fn partial_cmp(&self, other: &Foo<T>) -> Option<Ordering> {
        match self {
            Foo::Struct0 {} => match other {
                Foo::Struct0 {} => ...,
                Foo::Struct1 { .. } => ...,
                Foo::Struct2 { .. } => ...,
                Foo::Tuple0() => ...,
                Foo::Tuple1(..) => ...,
                Foo::Tuple2(..) => ...,
                Foo::Unit => ...,
            },
            Foo::Struct1 { .. } => match other {
                Foo::Struct0 {} => ...,
                Foo::Struct1 { .. } => ...,
                Foo::Struct2 { .. } => ...,
                Foo::Tuple0() => ...,
                Foo::Tuple1(..) => ...,
                Foo::Tuple2(..) => ...,
                Foo::Unit => ...,
            },
            Foo::Struct2 { .. } => match other {
                Foo::Struct0 {} => ...,
                Foo::Struct1 { .. } => ...,
                Foo::Struct2 { .. } => ...,
                Foo::Tuple0() => ...,
                Foo::Tuple1(..) => ...,
                Foo::Tuple2(..) => ...,
                Foo::Unit => ...,
            },
            Foo::Tuple0() => match other {
                Foo::Struct0 {} => ...,
                Foo::Struct1 { .. } => ...,
                Foo::Struct2 { .. } => ...,
                Foo::Tuple0() => ...,
                Foo::Tuple1(..) => ...,
                Foo::Tuple2(..) => ...,
                Foo::Unit => ...,
            },
            Foo::Tuple1(..) => match other {
                Foo::Struct0 {} => ...,
                Foo::Struct1 { .. } => ...,
                Foo::Struct2 { .. } => ...,
                Foo::Tuple0() => ...,
                Foo::Tuple1(..) => ...,
                Foo::Tuple2(..) => ...,
                Foo::Unit => ...,
            },
            Foo::Tuple2(..) => match other {
                Foo::Struct0 {} => ...,
                Foo::Struct1 { .. } => ...,
                Foo::Struct2 { .. } => ...,
                Foo::Tuple0() => ...,
                Foo::Tuple1(..) => ...,
                Foo::Tuple2(..) => ...,
                Foo::Unit => ...,
            },
            Foo::Unit => match other {
                Foo::Struct0 {} => ...,
                Foo::Struct1 { .. } => ...,
                Foo::Struct2 { .. } => ...,
                Foo::Tuple0() => ...,
                Foo::Tuple1(..) => ...,
                Foo::Tuple2(..) => ...,
                Foo::Unit => ...,
            },
        }
    }
}

What do you recommend for implementing PartialOrd for enums like this? Also, what’s preventing Discriminant from implementing PartialOrd?

1 Like

Add a method that returns an integer for each variant then compare the integers.

The integer value of the actual discriminant is considered an unstable implementation detail, so I'd agree with @Coding-Badly that creating your own "discriminant" function would be the way to go here.

3 Likes

I think something like this should also work, if you consider this an acceptable number of branches:

impl<T: PartialEq> PartialOrd for Foo<T> {
    fn partial_cmp(&self, other: &Foo<T>) -> Option<Ordering> {
        match (self, other) {
            (Self::Struct0 {}, Self::Struct0 {}) => todo!(),
            (Self::Struct1 { .. }, Self::Struct1 { .. }) => todo!(),
            (Self::Struct2 { .. }, Self::Struct2 { .. }) => todo!(),
            (Self::Tuple0(), Self::Tuple0()) => todo!(),
            (Self::Tuple1(..), Self::Tuple1(..)) => todo!(),
            (Self::Tuple2(..), Self::Tuple2(..)) => todo!(),
            (Self::Unit, Self::Unit) => todo!(),
            
            (Self::Struct0 {}, _) => Some(Ordering::Less),
            (_, Self::Struct0 {}) => Some(Ordering::Greater),
            (Self::Struct1 { .. }, _) => Some(Ordering::Less),
            (_, Self::Struct1 { .. }) => Some(Ordering::Greater),
            (Self::Struct2 { .. }, _) => Some(Ordering::Less),
            (_, Self::Struct2 { .. }) => Some(Ordering::Greater),
            (Self::Tuple0(), _) => Some(Ordering::Less),
            (_, Self::Tuple0()) => Some(Ordering::Greater),
            (Self::Tuple1(..), _) => Some(Ordering::Less),
            (_, Self::Tuple1(..)) => Some(Ordering::Greater),
            (Self::Tuple2(..), _) => Some(Ordering::Less),
            (_, Self::Tuple2(..)) => Some(Ordering::Greater),
            (Self::Unit, _) => Some(Ordering::Less),
            (_, Self::Unit) => Some(Ordering::Greater),
        }
    }
}

Remember that if you're just being PartialOrd, not Ord, you can choose to have different variants compare as None, which is much simpler to generate code for.

Whether you want that or not I don't know -- it seems uncommon in the ecosystem -- but personally I think it's often good, since whether Struct0 or Tuple0 or Unit is smallest seems entirely arbitrary to me.

SemVer concerns. Right now I can reorder my enum without breaking your code. But if you can be looking at the relative order of the variants of my enum using mem::Discriminant, that'd be a breaking change.

There's work on some sort of AsRepr only-derivable trait that will be the way to expose this in future.

You already can't reorder fieldless enums without breaking semver, right? MyEnum::VariantA as u32 already allows observing the discriminant as integer.

Field-less correct. But as doesn't currently work for anything with fields, like the one in this example.

I don’t actually need the integer value, knowing the relative order of the discriminants is enough. I mean, Discriminant already implements Eq and PartialEq, why not implement Ord and PartialOrd? Is is an oversight or is it by design?

I want the same behavior as the standard library PartialEq macro. As for semver concerns, like @bjorn3 said, reordering variant already breaks semver, but both for fieldless enums and enums with fields. If I implement PartialOrd by deriving the standard library PartialEq macro, reordering variants already changes behavior.

Actually, I didn’t tell the whole story, the thing is I am writing some derive macros that behaves like the standard library ones (Clone, Default, Hash, PartialEq, etc.), but does not require generic arguments having the same constraints. I got stuck on implementing my own PartialOrd and Ord macros. The standard library have access to the ordering of variants, but not for me. I am wondering:

  1. It is possible to write my derive macros that behaves like PartialOrd and Ord macros, but ignoring generic constraints?
  2. If not currently possible, is it OK to implement PartialOrd and Ord for Discriminant so I can do this in the future?

If a foreign enum has fields on any of its variants, and doesn't implement PartialOrd, then you can't use #[derive(PartialOrd)] on a local type to observe the order of its fields. Therefore, the only way to observe the field order is to use something explicitly unstable like the Hash impl on Discriminant.

Yes, create a private pseudo-discriminant function based on the known variant order, then compare the values it returns.

The standard library PartialOrd relies on the real discriminant values, how to write the pseudo discriminant function that behaves like the unstable discriminant_value function? I think it is wrong to base the pseudo discriminant value on variant declaration order, because user can assign a certain value as the discriminant value, which may cause the order of pseudo discriminants being different with the real discriminants.

If you want the pseudo-discriminant to act like the real discriminant for fieldless enums, or for #[repr(Int)] enums with explicit discriminants, you can replicate the algorithm specified for the discriminant for those (viz., start with 0, increment by 1 each variant, and reset at each explicit discriminant).

Only if you used the derived (Partial)Ord. You're not forced to do that, and in fact I sometimes argue against doing it because of its general meaninglessness, like