Consider splitting [Partial]Eq/Ord into separate traits/operators

Would it be possible/practical/desirable to split PartialEq/PartialOrd/Eq/Ord into separate traits that implement the various operators (==, !=, >, >=, <, <=) individually? The operators could be overloaded and return different results depending on an Output associated type. You could choose to implement PartialOrd/PartialEq on types that implement all the required traits and have type Output = bool;

For example, here's how PartialEq and Eq could be written:

trait Equal<Rhs = Self>
where
    Rhs: ?Sized,
{
    type Output;
    fn eq(&self, other: &Rhs) -> Self::Output;
}

trait NotEqual<Rhs = Self>
where
    Rhs: ?Sized,
{
    type Output;
    fn ne(&self, other: &Rhs) -> Self::Output;
}

trait PartialEq<Rhs = Self>: Equal<Rhs, Output = bool> + NotEqual<Rhs, Output = bool>
where
    Rhs: ?Sized,
{
    // could provide a stub eq & ne functions that call Equal::eq and NotEqual::ne
}

trait Eq: PartialEq<Self> {}

If #![feature(associated_type_defaults)] was enabled, the traits could provide a reasonable default value of type Output = bool;. The function name could also be more general (eg. Equal::op and NotEqual::op instead of Equal::eq and NotEqual::ne) if preferable.

I won't post the full code for the PartialOrd and Ord traits, but they could be written in a similar way:

trait PartialOrd<Rhs = Self>:
    PartialEq<Rhs>
    + GreaterThan<Rhs, Output = bool>
    + GreaterOrEqual<Rhs, Output = bool>
    + LessThan<Rhs, Output = bool>
    + LessOrEqual<Rhs, Output = bool>
where
    Rhs: ?Sized,
{
    // Required method
    fn partial_cmp(&self, other: &Rhs) -> Option<Ordering>;
}

trait Ord: PartialOrd<Self> {
    // Required method
    fn cmp(&self, other: &Self) -> Ordering; 
    // Provided methods
   # snip #
}

Implementing these traits for a type would be straightforward:

impl Equal for i32 {
    type Output = bool; // line not needed if associated_type_defaults used
    fn eq(&self, other: &i32) -> bool {
        *self == *other // might need to change if this trait really did implement `==`
    }
}

impl NotEqual for i32 {
    type Output = bool;
    fn ne(&self, _other: &i32) -> bool {
        !Equal::eq(self, _other)
    }
}

impl PartialEq for i32 {}
impl Eq for i32 {}

The pain would be in transitioning between the designs and to reduce the pain it could require deprecations, dummy methods and perhaps auto assisted changes to manually written code. Any derived PartialEq/PartialOrd/Eq/Ord should be able to just produce different output and be a transparent change.

Another alternative design could be that Equal/NotEqual/GreaterThan/GreaterOrEqual/LessThan/LessOrEqual traits are only allowed to be implemented on types that don't implement PartialEq/Eq/PartialOrd/Ord and both sets of functions override the operators, and although it would be less disruptive to existing code, personally I don't like this method. There may also be other ways to implement this that I haven't thought of. At first I thought about suggesting to add an associated type to the existing PartialEq/PartialOrd traits, but it didn't make much sense for these types to return anything but bool as these traits are really only about determining a partial/total order.

My initial motivation comes from wanting to be able to define relationships between mathematical expressions in a computer algebra system a clear way. Instead of using lots of method calls like this:

let x = Symbol::new('x');
let solution = solve([(x + 2).equals(x.pow(2)), (x).greater_than(4), (x).less_or_equal(20)], [x]);
//or
let solution = solve([(x + 2).eq(x.pow(2)), (x).gt(4), (x).le(20)], [x]);

With overloadable relationship operators it would be possible to overload the operators between expressions to return a Relationship enum (which holds both left and right expressions) instead of a bool and tidy up the design:

let x = symbol('x');
let solution = solve([x + 2 == x.pow(2), x > 4, x <= 20], [x]);

// or, if && and || were overloadable, then perhaps:
let condition = (x + 2 == x.pow(2)) && (x > 4) && (x <= 20);
let solution = solve(&condition, [x]);
// ps. where's the discussion about the design of
// the && and || short circuit operators mentioned in the docs?

Operator overloading for ==, !=, >, >=, <, <= without the restriction of returning bool and the consistency requirements could perhaps be useful in other situations too, although maybe it could lead to some exotic looking code if abused.

PartialOrd having the operator traits as supertraits is a no-go. Almost always it's vastly simpler and less boiler-platey to (auto-)implement the operators in terms of PartialOrd, not the other way around. And such a breaking of a vast amount of currently-valid Rust code is anyway completely out of question, no matter how mechanical or automatizable the fix was. [Edit: though granted it's also possible to impl a supertrait in terms of a subtrait.]

Your alternative way is perhaps more reasonable, at least in theory. A type that has both .cmp() doing normal comparison and the <=> operators overloaded to do something special would be really confusing. In this case, [Partial]Ord would remain the normal way to implement ordering, and the individual operator traits could be reserved for niche use cases. I'm very doubtful, however, that said niche use cases would justify adding complexity and potential confusion to the language.

If your boundary conditions are reasonably simple, a workaround is to write a macro that rewrites the comparison operators into the equivalent constraint objects. Macros are fairly commonly used to DSLs like that in Rust.

5 Likes

I think Rust operators aren't meant to be overloaded into some DSL-esque usage. You can feel that from the trait naming. It's called Add, not Plus or operator+.

Sure something like spirit v3 in C++ looks cool, but similar works in Rust are mostly implemented in macros and I think it works fine.

9 Likes

While expression templates are neat, I don't think I'd trade them for the current set of operator traits due to the "trust" they allow me to currently have when reading code. Not to mention that in your example,

would need to have that first argument have consistent types, probably Box<dyn Expression> which would require wrapping in Box::new(arg) as Box<dyn Expression> (though maybe type inference can elide the as part). I suppose you could have this be a huge enum, but then it would be full of Boxes due to the recursive nature of just about every operator. I feel like a macro that matches on the tokens of the operators and builds the tree that way would work better myself. You could even then have x**2 instead of x.pow(2). Or 2**x instead of the (otherwise impossible) 2.pow(x).

See these threads:

Prior discussion:

3 Likes

I am writing a Tensor lib, which has similar goals which is to achieve better ergonomic APIs.

Instead of splitting existing PartialOrd and PartialEq trait. To allow overload ==, !=, >, >=, <, <=. We can

  1. Introduce separate Trait for each operator just like ops::Add in std.
  2. And provide default implementation for T: PartialOrd and PartialEq.
  3. Change ==, !=, >, >=, <, <= overload to use new trait instead of PartialOrd and PartialEq

This approach should be minimum impact to existing code base while open opportunity for lib author to create more ergonomic API for their lib.

// type impl Greater to overload > operator, e.g lhs > rhs
pub trait Greater<Rhs = Self> 
where
    Rhs: ?Sized
{
    type Output;
    fn greater(&self, rhs: &Rhs) -> Self::Output;
}

// default implment for PartialOrd
impl<T, Rhs> Greater<Rhs> for T
where
    T: PartialOrd<Rhs>,
{
    type Output = bool;
    fn greater(&self, rhs: &Rhs) -> Self::Output {
        self.gt(rhs)
    }
}

// impl operator for user types
pub struct Symbol;

impl Greater for Symbol {
    type Output = Symbol;
    fn greater(&self, rhs: &Symbol) -> Self::Output {
        Symbol
    }
}

Having PartialEq and PartialOrd return non-bool types is extremely unlikely to happen. Rust's operator overloading traits are specifically designed wherever possible to steer people towards using them as semantically meaningful operators rather than general-purpose constructs, and returning bool is part of that.

As others suggested, consider writing a macro instead, which can accept these operators in its arguments and parse them in an arbitrary way.

5 Likes