My trait and PartialEq

Hi all, I've read through the existing threads and can't find anything....

I want to declare that my Trait should be comparable and debuggable, but I can't figure out how:

// every implementation of this _should_ implement PartialEq
pub trait IEvent: std::fmt::Debug {} 

pub trait ICanExecute<C: ICommand> {
    fn execute(&mut self, command: C) -> Result<Vec<Box<dyn IEvent>>, Box<dyn Error>>;
}

#[cfg(test)]
mod sanity {
    // this isn't doing anything other than checking everything compiles correctly ;-)
    use super::*;

    #[derive(Debug, PartialEq)]
    struct EventOne {
        a: usize,
    }
    impl IEvent for EventOne {}
    #[derive(Debug, PartialEq)]
    struct EventTwo {
        b: usize,
    }
    impl IEvent for EventTwo {}
    #[derive(Debug, PartialEq)]
    struct CommandOne {}
    impl ICommand for CommandOne {}

    struct ICanExecuteDomainOne {}
    impl<C: ICommand> ICanExecute<C> for ICanExecuteDomainOne {
        fn execute(&mut self, _command: C) -> Result<Vec<Box<dyn IEvent>>, Box<dyn Error>> {
            Ok(vec![
                Box::new(EventOne { a: 1 }),
                Box::new(EventTwo { b: 2 }),
            ])
        }
    }

    #[test]
    fn sanity_check() {
        let mut c = ICanExecuteDomainOne {};
        let expected: Vec<Box<dyn IEvent>> =
            vec![Box::new(EventOne { a: 1 }), Box::new(EventTwo { b: 2 })];
        let actual = c.execute(CommandOne {}).unwrap();
// this won't compile...
//        assert_eq!(expected, actual);
        assert_eq!(expected.len(), actual.len());
    }
}

I've naively tried:

pub trait IEvent: std::cmp::PartialEq<dyn IEvent> {}

but that doesn't work. Neither does:

pub trait ICanExecute<C: ICommand> {
    fn execute(&mut self, command: C) -> Result<Vec<Box<dyn IEvent + std::cmp::PartialEq<dyn Event>>>, Box<dyn Error>>;
}

What am I (intellectually) missing here?

For reference, the compiler error this produces is:

error[E0391]: cycle detected when computing the super predicates of `IEvent`
 --> src/lib.rs:5:1
  |
5 | pub trait IEvent: std::fmt::Debug + std::cmp::PartialEq<dyn IEvent> {}
  | ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
  |
  = note: ...which immediately requires computing the super predicates of `IEvent` again
note: cycle used when collecting item types in top-level module
 --> src/lib.rs:5:1
  |
5 | pub trait IEvent: std::fmt::Debug + std::cmp::PartialEq<dyn IEvent> {}
  | ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
1 Like

The fundamental issue is that the #[derive(PartialEq)] only implements PartialEq<Self>, which is distinct from PartialEq<dyn SomeTrait>. Making this work is possible, but can get a bit complicated; see @quinedot's dyn Trait guide for an example.

2 Likes

Thanks @2e71828 - that makes a lot of sense. It feels like it should be significantly simpler than this. Thanks for that link - full of good stuff :-).

Rust leans on concrete (possibly generic) types a lot more than other languages. Without knowing the details of what you're trying to do, I'd probably try to use an enum Event instead of dyn IEvent; then #[derive(PartialEq)] would "just work." Unfortunately, that isn't always possible.


Another option, which has its own set of tradeoffs, would be something like this:

pub trait ICanExecute<C: ICommand> {
    type Event: Debug + PartialOrd;
    fn execute(&mut self, command: C) -> Result<Vec<Self::Event>>, Box<dyn Error>>;
}

That would let each command domain define its own Event enum, potentially independent of the others.

1 Like

What do you expect to be simpler, concretely? Rust generally doesn't make anything more complicated than absolutely necessary. It is likely that you are misunderstanding how generics work.


If you merely want to compare two trait objects for equality, you can use my crate dyn_ord.

Doesn't a self type (e.g. type Event: Debug + PartialOrd) require that the result contains a Vec of the same "realised Event" (I'm lacking the words), so I couldn't return a Vec containing EventOne and EventTwo?

Hi @H2CO3, by "simpler" I mean it felt like an acceptable assumption that I can declare that things that implement my own trait are also comparable. The simple and intuitive way to do that is something like trait MyTrait: Debug + PartialEq, which isn't possible (and neither is ...+PartialEq<Self> or ...+PartialEq<MyTrait>.

I "get" generics, and now I understand the reasoning, it is clear why Rust can't do it, but to "make it work" requires complex tradeoffs (like those listed in dyn PartialEq - Learning Rust).

(It doesn't help that I've got a decade+ of OOP experience)

The real problem is that what I'm trying to do (use 'marker interfaces' for data) isn't idiomatic Rust:

  • traits are not inheritence, so Liskov's rules of substitutions don't hold
  • traits are defining behaviour, not "marker" traits for data

My brain will stop trying to write OO one day soon....

1 Like

That's correct as far as it goes, but you can return a vector of enum DomainEvent with variants for the two choices. As long as the set of valid events is closed for any given domain/command pairing, you should be good. You can also specify Event = Box<dyn ...> if you have a domain without that closed set, but you'd need to bring in the PartialEq machinery at that point.

One of the trickier parts of Rust API design is deciding on the right combination of enums, monomorphized generics, and type erasure in any given circumstance. Flexibility is good, but it comes hand-in-hand with complexity and striking the right balance between the two is incredibly hard sometimes. And some common cases, like equality testing type-erased values, turn out to be more complicated than they look at first glance.

1 Like

Exactly this. The first version does in fact use an enum Event and multiple large match event {...} expressions. I wanted to experiment with traits this time but alas :-).

It will be interesting when we can implement traits against enum members...I noticed a few RFCs around that.

I'm not sure what exactly is "not possible" here. Are you trying to declare that a trait MyTrait must have PartialEq<U> as a supertrait, for all types U such that U: MyTrait? Or would you satisfied by exhaustive blanket impls for such types? (The latter is impossible not because of a limitation in Rust but due to coherence.)

I don't think either of this is true as-is:

  • Traits are not inheritance, but supertrait-subtrait relations exist, and a type parameter bounded by a subtrait can make use of all capabilities (methods, associated items) of its supertraits; and
  • It's perfectly fine and common to use traits as pure markers. The very core language does that, for example Send, Sync, and Unpin are merely markers with no methods, only tacit assumptions of structural properties of types that allow writing sound code (w.r.t/ thread-safe or self-referentiality, respectively).

So you probably have a different problem, which we could help you solve if you elaborated about the concrete use case, rather than something abstract like "I want equality to work easier".

1 Like

The original question was pretty clear I think - I want to define a trait so that all things that implement that trait are comparable with each other. "...U such that U: MyTrait' is sufficient.

I'm not sure you've disagreed with either of my points ;-). You agree they aren't inheritance (so X implements T isn't T) and marker types are fine - the key point is they are behaviour not just data.

Again, I think my first post was pretty concrete ;-).

Note that this suggests that you might not want a trait in the first place, because traits are primarily about being open for extensibility: I can implement your trait for my type, but you don't know about my type, making it hard for your types to be comparable to my type.

This is where the enum suggestions come from: they're closed so you know all the possible types that can be in the enum, and thus have a place to put the logic for the full quadratic set of combinations, without needing to worry about other types which you know nothing about.

6 Likes

Thanks @scottmcm. Exactly this. All of this pain comes from me wanting to misuse traits, which are about behaviour. My first version of this does actually use an enum but I wanted to get away from the matches that crept through the code base.

The code is clearly sending me a strong signal and I'm listening to it ;-). Back to enums it is.

1 Like

In a nominative type system like Rust has, it's arguably reasonable to expect that two values of distinct types are unequal to each other even after type erasure. The convention of transparent proxy types (e.g. &T) messes with that idea, though, making it a much more complicated proposition in Rust.

Were I to redesign Rust from scratch, I would be tempted to remove the type parameter from PartialEq and add some magic to make it work with trait objects.

The other way to look at this is that traits and enums are just two different ways to organize the table. If you imagine a table of types vertically vs functions horizontally, with the cells as the behaviour of that function for that type, then drawing circles around the rows is the trait organization and drawing circles around the columns is the enum organization.

It's all the same logic, just organized differently. enums make it easier to add functions but harder to add types; traits make it easier to add types but harder to add functions. And neither of those is fundamentally better than the other; you just have to decide which is less annoying for the situation you're in.

5 Likes

You may want to look into the enum_dispatch crate as a different alternative that fixes the same problem.

1 Like

thanks @riking - this might be just the ticket :wink:

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.